mirror of
https://github.com/lichess-org/mobile.git
synced 2026-05-26 13:50:52 +00:00
Backport fixes/features to 0.18.x (#2482)
* feat: update chessground to 7.3.0 (add a board (purple), pieces (xkcd, firi) and fixing staunty knight to be aligned center) (#2380) * Add purple board theme in list (chessground dependency) * Change chessground version to 7.3.0 * feat(otb): add takeback button (#1642) (#2334) * potential fix for notification issues on iOS (#2451) getAPNSToken() is the only iOS specific thing that happens in this file, so I think there's a high probabillity that the issues are related to that. According to https://firebase.google.com/docs/cloud-messaging/flutter/get-started we need to get permission *before* calling getAPNSToken(). Currently it looks like there could be a race condition where the connectivity change listener fires before we call requestPermission(). Fix this by moving the ref.listen() below the requestPermission() call. * Fix register firebase after sign in And make sure _registeredDevice flag is only set when device is actually registered. * fix: remove delay when entering analysis board (#2471) This is an alternate fix for #1232 which does not need the 500ms delay. When entering the analysis board, the delay of 500ms before scrolling to the current move is unexpected for users. With this solution, we can jump to the current move immedialtely. * feat: add log screen (#2463) --------- Co-authored-by: lumiknit <aasr4r4@gmail.com> Co-authored-by: overcharged <199525476+overcharged-coder@users.noreply.github.com> Co-authored-by: Vincent Velociter <vincent.velociter@gmail.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:l10n_esperanto/l10n_esperanto.dart';
|
||||
import 'package:lichess_mobile/l10n/l10n.dart';
|
||||
import 'package:lichess_mobile/src/app_links.dart';
|
||||
import 'package:lichess_mobile/src/log.dart';
|
||||
import 'package:lichess_mobile/src/model/account/account_service.dart';
|
||||
import 'package:lichess_mobile/src/model/account/ongoing_game.dart';
|
||||
import 'package:lichess_mobile/src/model/challenge/challenge_service.dart';
|
||||
@@ -65,6 +66,7 @@ class _AppState extends ConsumerState<Application> {
|
||||
@override
|
||||
void initState() {
|
||||
// Start services
|
||||
ref.read(appLogStorageServiceProvider).start();
|
||||
ref.read(notificationServiceProvider).start();
|
||||
ref.read(messageServiceProvider).start();
|
||||
ref.read(challengeServiceProvider).start();
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:lichess_mobile/firebase_options.dart';
|
||||
import 'package:lichess_mobile/src/log.dart';
|
||||
import 'package:lichess_mobile/src/model/engine/engine.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -89,9 +88,7 @@ abstract class LichessBinding {
|
||||
|
||||
/// A concrete implementation of [LichessBinding] for the app.
|
||||
class AppLichessBinding extends LichessBinding {
|
||||
AppLichessBinding() {
|
||||
setupLogging();
|
||||
}
|
||||
AppLichessBinding();
|
||||
|
||||
/// Returns an instance of the binding that implements [LichessBinding].
|
||||
///
|
||||
|
||||
+46
-15
@@ -2,29 +2,60 @@ import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lichess_mobile/src/model/settings/log_preferences.dart';
|
||||
import 'package:lichess_mobile/src/utils/lru_list.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const _loggersToShowInTerminal = {'HttpClient', 'Socket', 'EvaluationService'};
|
||||
|
||||
/// Setup logging
|
||||
void setupLogging() {
|
||||
if (kDebugMode) {
|
||||
Logger.root.level = Level.FINE;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
developer.log(
|
||||
record.message,
|
||||
time: record.time,
|
||||
name: record.loggerName,
|
||||
level: record.level.value,
|
||||
error: record.error,
|
||||
stackTrace: record.stackTrace,
|
||||
);
|
||||
/// Provides an instance of [AppLogStorageService] using Riverpod.
|
||||
final appLogStorageServiceProvider = Provider<AppLogStorageService>(
|
||||
(Ref ref) => AppLogStorageService(ref),
|
||||
name: 'AppLogStorageServiceProvider',
|
||||
);
|
||||
|
||||
if (_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.FINE) {
|
||||
debugPrint('[${record.loggerName}] ${record.message}');
|
||||
/// Manages log entries created via [Logger] instances
|
||||
///
|
||||
/// Currently, simply saves the most recent log entries in memory, so they do not persists across app restarts.
|
||||
class AppLogStorageService {
|
||||
AppLogStorageService(this.ref);
|
||||
|
||||
final Ref ref;
|
||||
final _logs = LRUList<LogRecord>(capacity: 1024);
|
||||
|
||||
/// Currently stored log entries, ordered from oldest to newest.
|
||||
Iterable<LogRecord> get logs => _logs.values;
|
||||
|
||||
void start() {
|
||||
ref.listen(logPreferencesProvider.select((prefs) => prefs.level), (prev, next) {
|
||||
if (next != prev) {
|
||||
Logger.root.level = next;
|
||||
}
|
||||
}, fireImmediately: true);
|
||||
|
||||
Logger.root.onRecord.listen((record) {
|
||||
if (kDebugMode) {
|
||||
developer.log(
|
||||
record.message,
|
||||
time: record.time,
|
||||
name: record.loggerName,
|
||||
level: record.level.value,
|
||||
error: record.error,
|
||||
stackTrace: record.stackTrace,
|
||||
);
|
||||
|
||||
if (_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.FINE) {
|
||||
debugPrint('[${record.loggerName}] ${record.message}');
|
||||
}
|
||||
}
|
||||
|
||||
_logs.put(record);
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_logs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderLogger extends ProviderObserver {
|
||||
|
||||
@@ -98,18 +98,6 @@ class NotificationService {
|
||||
/// This method should be called once the app is ready to receive notifications,
|
||||
/// and after [LichessBinding.initializeNotifications] has been called.
|
||||
Future<void> start() async {
|
||||
// listen for connectivity changes to register device once the app is online
|
||||
_connectivitySubscription = _ref.listen(connectivityChangesProvider, (prev, current) async {
|
||||
if (current.value?.isOnline == true && !_registeredDevice) {
|
||||
try {
|
||||
await registerDevice();
|
||||
_registeredDevice = true;
|
||||
} catch (e, st) {
|
||||
_logger.severe('Could not setup push notifications; $e\n$st');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for incoming messages while the app is in the foreground.
|
||||
LichessBinding.instance.firebaseMessagingOnMessage.listen((RemoteMessage message) {
|
||||
_processFcmMessage(message, fromBackground: false);
|
||||
@@ -139,6 +127,20 @@ class NotificationService {
|
||||
_registerToken(token);
|
||||
});
|
||||
|
||||
// listen for connectivity changes to register device once the app is online
|
||||
// This needs to be done *after* via have gotten permission, otherwise on iOS
|
||||
// getAPNSToken() might still return null.
|
||||
_connectivitySubscription = _ref.listen(connectivityChangesProvider, (prev, current) async {
|
||||
if (current.value?.isOnline == true && !_registeredDevice) {
|
||||
try {
|
||||
final success = await registerDevice();
|
||||
if (success) _registeredDevice = true;
|
||||
} catch (e, st) {
|
||||
_logger.severe('Could not setup push notifications; $e\n$st');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get any messages which caused the application to open from
|
||||
// a terminated state.
|
||||
final RemoteMessage? initialMessage = await LichessBinding.instance.firebaseMessaging
|
||||
@@ -326,18 +328,23 @@ class NotificationService {
|
||||
}
|
||||
|
||||
/// Register the device for push notifications.
|
||||
Future<void> registerDevice() async {
|
||||
///
|
||||
/// Returns true if the device was successfully registered, false otherwise.
|
||||
Future<bool> registerDevice() async {
|
||||
// For apple platforms, make sure the APNS token is available before making any FCM plugin API calls
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
final apnsToken = await LichessBinding.instance.firebaseMessaging.getAPNSToken();
|
||||
if (apnsToken == null) {
|
||||
_logger.warning('APNS token is null');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final token = await LichessBinding.instance.firebaseMessaging.getToken();
|
||||
if (token != null) {
|
||||
await _registerToken(token);
|
||||
if (token == null) {
|
||||
_logger.warning('FCM token is null');
|
||||
return false;
|
||||
}
|
||||
return await _registerToken(token);
|
||||
}
|
||||
|
||||
/// Unregister the device from push notifications.
|
||||
@@ -354,20 +361,22 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _registerToken(String token) async {
|
||||
Future<bool> _registerToken(String token) async {
|
||||
final settings = await LichessBinding.instance.firebaseMessaging.getNotificationSettings();
|
||||
if (settings.authorizationStatus == AuthorizationStatus.denied) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
_logger.info('will register fcmToken: $token');
|
||||
final session = _ref.read(authSessionProvider);
|
||||
if (session == null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await _ref.withClient((client) => client.post(Uri(path: '/mobile/register/firebase/$token')));
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
_logger.severe('could not register device; $e', e, st);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +318,7 @@ enum BoardTheme {
|
||||
metal('Metal', 'metal'),
|
||||
olive('Olive', 'olive'),
|
||||
newspaper('Newspaper', 'newspaper'),
|
||||
purple('Purple', 'purple'),
|
||||
purpleDiag('Purple-Diag', 'purple-diag'),
|
||||
pinkPyramid('Pink', 'pink'),
|
||||
horsey('Horsey', 'horsey');
|
||||
@@ -373,6 +374,8 @@ enum BoardTheme {
|
||||
return ChessboardColorScheme.olive;
|
||||
case BoardTheme.newspaper:
|
||||
return ChessboardColorScheme.newspaper;
|
||||
case BoardTheme.purple:
|
||||
return ChessboardColorScheme.purple;
|
||||
case BoardTheme.purpleDiag:
|
||||
return ChessboardColorScheme.purpleDiag;
|
||||
case BoardTheme.pinkPyramid:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
part 'log_preferences.freezed.dart';
|
||||
part 'log_preferences.g.dart';
|
||||
|
||||
final logPreferencesProvider = NotifierProvider<LogPreferencesNotifier, LogPrefs>(
|
||||
LogPreferencesNotifier.new,
|
||||
name: 'LogPreferencesProvider',
|
||||
);
|
||||
|
||||
class LogPreferencesNotifier extends Notifier<LogPrefs> with PreferencesStorage<LogPrefs> {
|
||||
@override
|
||||
@protected
|
||||
PrefCategory get prefCategory => PrefCategory.overTheBoard;
|
||||
|
||||
@override
|
||||
@protected
|
||||
LogPrefs get defaults => LogPrefs.defaults;
|
||||
|
||||
@override
|
||||
LogPrefs fromJson(Map<String, dynamic> json) => LogPrefs.fromJson(json);
|
||||
|
||||
@override
|
||||
LogPrefs build() => fetch();
|
||||
|
||||
Future<void> setLogLevel(Level level) {
|
||||
return save(state.copyWith(level: level));
|
||||
}
|
||||
}
|
||||
|
||||
const _kDefaultLevel = kDebugMode ? Level.FINE : Level.WARNING;
|
||||
|
||||
@Freezed(fromJson: true, toJson: true)
|
||||
sealed class LogPrefs with _$LogPrefs implements Serializable {
|
||||
const LogPrefs._();
|
||||
|
||||
const factory LogPrefs({@LevelConverter() required Level level}) = _LogPrefs;
|
||||
|
||||
static const defaults = LogPrefs(level: _kDefaultLevel);
|
||||
|
||||
factory LogPrefs.fromJson(Map<String, dynamic> json) {
|
||||
return _$LogPrefsFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
class LevelConverter implements JsonConverter<Level, String> {
|
||||
const LevelConverter();
|
||||
|
||||
@override
|
||||
Level fromJson(String json) {
|
||||
try {
|
||||
final value = int.parse(json);
|
||||
return Level.LEVELS.firstWhere((level) => level.value == value);
|
||||
} catch (e) {
|
||||
return _kDefaultLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson(Level object) => object.value.toString();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:collection';
|
||||
|
||||
/// A simple LRU (Least Recently Used) list implementation
|
||||
///
|
||||
/// The items are ordered by their insertion time, with the most recently added item at the end.
|
||||
/// If the list exceeds its capacity when extended via [put], the least recently added item is removed.
|
||||
class LRUList<T> {
|
||||
final int capacity;
|
||||
final LinkedList<_LRUListEntry<T>> _list = LinkedList<_LRUListEntry<T>>();
|
||||
|
||||
LRUList({required this.capacity});
|
||||
|
||||
/// Add the [value] to the end of the list. If the list exceeds its capacity, the least recently used item is removed.
|
||||
void put(T value) {
|
||||
if (_list.length >= capacity) {
|
||||
_list.first.unlink();
|
||||
}
|
||||
_list.add(_LRUListEntry<T>(value));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_list.clear();
|
||||
}
|
||||
|
||||
/// The values in the list, ordered from least recently used to most recently used.
|
||||
Iterable<T> get values => _list.map((entry) => entry.value);
|
||||
}
|
||||
|
||||
final class _LRUListEntry<T> extends LinkedListEntry<_LRUListEntry<T>> {
|
||||
final T value;
|
||||
|
||||
_LRUListEntry(this.value);
|
||||
}
|
||||
@@ -243,7 +243,6 @@ class _BottomBar extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final gameState = ref.watch(overTheBoardGameControllerProvider);
|
||||
|
||||
final clock = ref.watch(overTheBoardClockProvider);
|
||||
|
||||
return BottomBar(
|
||||
@@ -297,6 +296,20 @@ class _BottomBar extends ConsumerWidget {
|
||||
: null,
|
||||
icon: CupertinoIcons.chevron_forward,
|
||||
),
|
||||
BottomBarButton(
|
||||
label: 'Takeback',
|
||||
onTap: gameState.canGoBack
|
||||
? () {
|
||||
ref.read(overTheBoardGameControllerProvider.notifier).goBack();
|
||||
if (clock.active) {
|
||||
ref
|
||||
.read(overTheBoardClockProvider.notifier)
|
||||
.switchSide(newSideToMove: gameState.turn.opposite, addIncrement: false);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
icon: Icons.undo,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -376,9 +389,7 @@ 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
|
||||
@@ -403,7 +414,6 @@ class _Player extends ConsumerWidget {
|
||||
timeLeft: Duration(milliseconds: max(0, clock.timeLeft(side)!.inMilliseconds)),
|
||||
key: clockKey,
|
||||
active: clock.activeClock == side,
|
||||
// https://github.com/lichess-org/mobile/issues/785#issuecomment-2183903498
|
||||
emergencyThreshold: Duration(
|
||||
seconds: (clock.timeIncrement.time * 0.125).clamp(10, 60).toInt(),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
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/log.dart';
|
||||
import 'package:lichess_mobile/src/model/settings/log_preferences.dart';
|
||||
import 'package:lichess_mobile/src/styles/styles.dart';
|
||||
import 'package:lichess_mobile/src/utils/navigation.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_choice_picker.dart';
|
||||
import 'package:lichess_mobile/src/widgets/list.dart';
|
||||
import 'package:lichess_mobile/src/widgets/settings.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
final Logger _logger = Logger('AppLogSettingsScreen');
|
||||
|
||||
final _logDateFormatter = DateFormat.Hms();
|
||||
|
||||
class AppLogSettingsScreen extends ConsumerWidget {
|
||||
const AppLogSettingsScreen({super.key});
|
||||
|
||||
static Route<dynamic> buildRoute(BuildContext context) {
|
||||
return buildScreenRoute(context, screen: const AppLogSettingsScreen());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentLevel = ref.watch(logPreferencesProvider.select((prefs) => prefs.level));
|
||||
final logs = ref.read(appLogStorageServiceProvider).logs.toIList().reversed;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('App Logs'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Export',
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () => launchShareDialog(
|
||||
context,
|
||||
ShareParams(
|
||||
text: logs
|
||||
.map(
|
||||
(record) =>
|
||||
'[${_logDateFormatter.format(record.time)}] [${record.loggerName}] ${record.message}',
|
||||
)
|
||||
.join('\n'),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Delete all logs',
|
||||
icon: const Icon(Icons.delete_sweep),
|
||||
onPressed: () {
|
||||
showConfirmDialog<dynamic>(
|
||||
context,
|
||||
title: const Text('Delete all logs'),
|
||||
onConfirm: ref.read(appLogStorageServiceProvider).clear,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListSection(
|
||||
children: [
|
||||
SettingsListTile(
|
||||
settingsLabel: const Text('Log Level'),
|
||||
settingsValue: currentLevel.name,
|
||||
onTap: () {
|
||||
showChoicePicker<Level>(
|
||||
context,
|
||||
choices: Level.LEVELS,
|
||||
selectedItem: currentLevel,
|
||||
labelBuilder: (Level l) => Text(l.name),
|
||||
onSelectedItemChanged: (Level value) {
|
||||
_logger.fine('Changing log level to ${value.name}');
|
||||
ref.read(logPreferencesProvider.notifier).setLogLevel(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (logs.isNotEmpty)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: Styles.bodySectionPadding,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListView.separated(
|
||||
itemCount: logs.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1, thickness: 0),
|
||||
itemBuilder: (_, index) => _LogTile(record: logs[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogTile extends StatelessWidget {
|
||||
const _LogTile({required this.record});
|
||||
|
||||
final LogRecord record;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: SizedBox(
|
||||
width: 30,
|
||||
child: Text(record.level.name, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
title: Text(
|
||||
'[${record.loggerName}] ${record.message}',
|
||||
style: const TextStyle(fontSize: 14, letterSpacing: -0.15),
|
||||
),
|
||||
subtitle: Text(
|
||||
_logDateFormatter.format(record.time),
|
||||
style: TextStyle(color: textShade(context, 0.7), fontSize: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart';
|
||||
import 'package:lichess_mobile/src/utils/navigation.dart';
|
||||
import 'package:lichess_mobile/src/view/home/home_tab_screen.dart';
|
||||
import 'package:lichess_mobile/src/view/settings/account_preferences_screen.dart';
|
||||
import 'package:lichess_mobile/src/view/settings/app_log_settings_screen.dart';
|
||||
import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart';
|
||||
import 'package:lichess_mobile/src/view/settings/engine_settings_screen.dart';
|
||||
import 'package:lichess_mobile/src/view/settings/http_log_screen.dart';
|
||||
@@ -172,6 +173,16 @@ class SettingsScreen extends ConsumerWidget {
|
||||
title: const Text('HTTP logs'),
|
||||
onTap: () => Navigator.push(context, HttpLogScreen.buildRoute(context)),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.bug_report),
|
||||
title: const Text('App Logs'),
|
||||
trailing: Theme.of(context).platform == TargetPlatform.iOS
|
||||
? const CupertinoListTileChevron()
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(AppLogSettingsScreen.buildRoute(context));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
|
||||
+10
-15
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:chessground/chessground.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartchess/dartchess.dart';
|
||||
@@ -176,31 +174,28 @@ class _DebouncedPgnTreeViewState extends ConsumerState<DebouncedPgnTreeView> {
|
||||
/// When widget.livePath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every received move.
|
||||
late UciPath? pathToLiveMove;
|
||||
|
||||
Timer? _scrollTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
pathToCurrentMove = widget.currentPath;
|
||||
pathToLiveMove = widget.livePath;
|
||||
|
||||
// Scrollable.ensureVisible breaks animation when swiping between tabs (see https://github.com/lichess-org/mobile/issues/1232)
|
||||
// Explicitly use the Scrollable of our TreeView instead, so that it won't affect the TabController's state.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollTimer?.cancel();
|
||||
_scrollTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
if (currentMoveKey.currentContext != null) {
|
||||
Scrollable.ensureVisible(
|
||||
currentMoveKey.currentContext!,
|
||||
alignment: 0.5,
|
||||
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (currentMoveKey.currentContext != null) {
|
||||
Scrollable.of(currentMoveKey.currentContext!).position.ensureVisible(
|
||||
currentMoveKey.currentContext!.findRenderObject()!,
|
||||
alignment: 0.5,
|
||||
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce.cancel();
|
||||
_scrollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -205,10 +205,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chessground
|
||||
sha256: "61f95b9186eef3e232c2e95789b5631342c8019542c5529e181f72da37857684"
|
||||
sha256: "7f706292af797037f55277e256726c2ad2a271cbbb9760959064dc7a376b6ea5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
version: "7.3.0"
|
||||
ci:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencies:
|
||||
async: ^2.10.0
|
||||
auto_size_text: ^3.0.0
|
||||
cached_network_image: ^3.2.2
|
||||
chessground: ^7.2.0
|
||||
chessground: ^7.3.0
|
||||
clock: ^1.1.1
|
||||
collection: ^1.17.0
|
||||
connectivity_plus: ^7.0.0
|
||||
|
||||
Reference in New Issue
Block a user