Added Player setting

This commit is contained in:
kodjomoustapha 2023-12-21 19:12:55 +01:00
parent 3cba49e47e
commit 9033f17de3
22 changed files with 1203 additions and 197 deletions

View file

@ -268,5 +268,9 @@
"by_name": "حسب الاسم",
"installed": "مثبت",
"auto_scroll": "التمرير التلقائي",
"video_audio": "الصوت"
"video_audio": "الصوت",
"player": "لاعب",
"markEpisodeAsSeenSetting": "في أي نقطة لوضع علامة على الحلقة كمشاهدة",
"default_skip_intro_length": "طول تخطي المقدمة الافتراضي",
"default_playback_speed_length": "طول سرعة التشغيل الافتراضية"
}

View file

@ -268,5 +268,9 @@
"by_name": "By name",
"installed": "Installed",
"auto_scroll": "Auto scroll",
"video_audio": "Audio"
"video_audio": "Audio",
"player": "Player",
"markEpisodeAsSeenSetting": "At what point to mark the episode as seen",
"default_skip_intro_length": "Default Skip intro length",
"default_playback_speed_length": "Default Playback speed length"
}

View file

@ -268,5 +268,9 @@
"by_name": "Por nombre",
"installed": "Instalado",
"auto_scroll": "Desplazamiento automático",
"video_audio": "Audio"
"video_audio": "Audio",
"player": "Jugador",
"markEpisodeAsSeenSetting": "En qué punto marcar el episodio como visto",
"default_skip_intro_length": "Duración predeterminada para saltar la introducción",
"default_playback_speed_length": "Duración predeterminada de la velocidad de reproducción"
}

View file

@ -268,5 +268,9 @@
"by_name": "Par nom",
"installed": "Installé",
"auto_scroll": "Défilement automatique",
"video_audio": "Audio"
"video_audio": "Audio",
"player": "Lecteur",
"markEpisodeAsSeenSetting": "À quel moment marquer l'épisode comme vu",
"default_skip_intro_length": "Longueur par défaut du passage de l'intro",
"default_playback_speed_length": "Longueur par défaut de la vitesse de lecture"
}

View file

@ -268,5 +268,9 @@
"by_name": "Berdasarkan nama",
"installed": "Terpasang",
"auto_scroll": "Gulir otomatis",
"video_audio": "Audio"
"video_audio": "Audio",
"player": "Pemain",
"markEpisodeAsSeenSetting": "Pada titik mana menandai episode sebagai terlihat",
"default_skip_intro_length": "Panjang lewati intro default",
"default_playback_speed_length": "Panjang kecepatan pemutaran default"
}

View file

@ -268,5 +268,9 @@
"by_name": "Per nome",
"installed": "Installato",
"auto_scroll": "Scorrimento automatico",
"video_audio": "Audio"
"video_audio": "Audio",
"player": "Giocatore",
"markEpisodeAsSeenSetting": "In quale momento contrassegnare l'episodio come visto",
"default_skip_intro_length": "Durata predefinita per saltare l'introduzione",
"default_playback_speed_length": "Durata predefinita per la velocità di riproduzione"
}

View file

@ -268,5 +268,9 @@
"by_name": "Por nome",
"installed": "Instalado",
"auto_scroll": "Auto rolagem",
"video_audio": "Áudio"
"video_audio": "Áudio",
"player": "Jogador",
"markEpisodeAsSeenSetting": "Em que ponto marcar o episódio como visto",
"default_skip_intro_length": "Duração padrão para pular a introdução",
"default_playback_speed_length": "Duração padrão da velocidade de reprodução"
}

View file

@ -268,5 +268,9 @@
"by_name": "Por nome",
"installed": "Instalado",
"auto_scroll": "Rolagem automática",
"video_audio": "Áudio"
"video_audio": "Áudio",
"player": "Jogador",
"markEpisodeAsSeenSetting": "Em que ponto marcar o episódio como visto",
"default_skip_intro_length": "Duração padrão para pular a introdução",
"default_playback_speed_length": "Duração padrão da velocidade de reprodução"
}

View file

@ -268,5 +268,9 @@
"by_name": "По имени",
"installed": "Установлено",
"auto_scroll": "Автопрокрутка",
"video_audio": "Аудио"
"video_audio": "Аудио",
"player": "Игрок",
"markEpisodeAsSeenSetting": "В какой момент отметить эпизод как просмотренный",
"default_skip_intro_length": "Стандартная длина пропуска вступления",
"default_playback_speed_length": "Стандартная длина скорости воспроизведения"
}

View file

@ -268,5 +268,9 @@
"by_name": "İsme Göre",
"installed": "Yüklendi",
"auto_scroll": "Otomatik Kaydırma",
"video_audio": "Ses"
"video_audio": "Ses",
"player": "Oyuncu",
"markEpisodeAsSeenSetting": "Bölümün izlendiği olarak işaretleneceği nokta",
"default_skip_intro_length": "Varsayılan Giriş Atla süresi",
"default_playback_speed_length": "Varsayılan Oynatma hızı süresi"
}

View file

@ -143,6 +143,14 @@ class Settings {
List<AutoScrollPages>? autoScrollPages;
int? markEpisodeAsSeenType;
int? defaultSkipIntroLength;
int? defaultDoubleTapToSkipLength;
double? defaultPlayBackSpeed;
Settings(
{this.id = 227,
this.displayType = DisplayType.compactGrid,
@ -204,7 +212,11 @@ class Settings {
this.autoBackupLocation,
this.startDatebackup,
this.usePageTapZones = true,
this.autoScrollPages});
this.autoScrollPages,
this.markEpisodeAsSeenType = 85,
this.defaultSkipIntroLength = 85,
this.defaultDoubleTapToSkipLength = 10,
this.defaultPlayBackSpeed = 1.0});
Settings.fromJson(Map<String, dynamic> json) {
animatePageTransitions = json['animatePageTransitions'];
@ -322,6 +334,10 @@ class Settings {
autoBackupLocation = json['autoBackupLocation'];
startDatebackup = json['startDatebackup'];
usePageTapZones = json['usePageTapZones'];
markEpisodeAsSeenType = json['markEpisodeAsSeenType'];
defaultSkipIntroLength = json['defaultSkipIntroLength'];
defaultDoubleTapToSkipLength = json['defaultDoubleTapToSkipLength'];
defaultPlayBackSpeed = json['defaultPlayBackSpeed'];
}
Map<String, dynamic> toJson() => {
@ -407,7 +423,11 @@ class Settings {
'backupFrequencyOptions': backupFrequencyOptions,
'autoBackupLocation': autoBackupLocation,
'startDatebackup': startDatebackup,
'usePageTapZones': usePageTapZones
'usePageTapZones': usePageTapZones,
'markEpisodeAsSeenType': markEpisodeAsSeenType,
'defaultSkipIntroLength': defaultSkipIntroLength,
'defaultDoubleTapToSkipLength': defaultDoubleTapToSkipLength,
'defaultPlayBackSpeed': defaultPlayBackSpeed
};
}

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/video.dart' as vid;
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/get_video_list.dart';
@ -224,6 +225,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
_currentTotalDurationSub;
_player.open(Media(_video.value!.videoTrack!.id,
httpHeaders: _video.value!.headers));
_setPlaybackSpeed(ref.read(defaultPlayBackSpeedStateProvider));
super.initState();
}
@ -667,19 +669,21 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
}
Widget _seekToWidget() {
final defaultSkipIntroLength =
ref.watch(defaultSkipIntroLengthStateProvider);
return SizedBox(
height: 30,
child: ElevatedButton(
onPressed: () async {
ref.read(_seekTo.notifier).state = 85;
ref.read(_seekTo.notifier).state = defaultSkipIntroLength;
ref.read(_showSeekTo.notifier).state = true;
await _player
.seek(Duration(seconds: _currentPosition.inSeconds + 85));
await _player.seek(Duration(
seconds: _currentPosition.inSeconds + defaultSkipIntroLength));
ref.read(_seekTo.notifier).state = 0;
ref.read(_showSeekTo.notifier).state = false;
},
child:
const Text("+85", style: TextStyle(fontWeight: FontWeight.bold))),
child: Text("+$defaultSkipIntroLength",
style: const TextStyle(fontWeight: FontWeight.bold))),
);
}

View file

@ -5,6 +5,7 @@ import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'anime_player_controller_provider.g.dart';
@ -148,9 +149,10 @@ class AnimeStreamController extends _$AnimeStreamController {
{bool save = false}) {
if (episode.isRead!) return;
if (incognitoMode) return;
final markEpisodeAsSeenType = ref.watch(markEpisodeAsSeenTypeStateProvider);
final isWatch = totalDuration != null
? duration.inSeconds >= (totalDuration.inSeconds - 120)
? duration.inSeconds >=
((totalDuration.inSeconds * markEpisodeAsSeenType) / 100).ceil()
: false;
if (isWatch || save) {
final ep = episode;

View file

@ -7,7 +7,7 @@ part of 'anime_player_controller_provider.dart';
// **************************************************************************
String _$animeStreamControllerHash() =>
r'017ad7ee068f16f2b43b2c47692987cd1d1f70d7';
r'05102c5a067600752d15236c270f5eb1ab553980';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'reader_controller_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$currentIndexHash() => r'c2b912af925d9efd3e36e7a810914ef11393c1da';
String _$currentIndexHash() => r'7cf7d12cc79f02fec4de750e4aedf5c9e09e5284';
/// Copied from Dart SDK
class _SystemHash {
@ -169,7 +169,7 @@ class _CurrentIndexProviderElement
Chapter get chapter => (origin as CurrentIndexProvider).chapter;
}
String _$readerControllerHash() => r'611b6eca40a398fe9c71911db2ca9714d6cc05a0';
String _$readerControllerHash() => r'e6191b82e9cbbe2856c4c46b407ccd5da9206c8e';
abstract class _$ReaderController extends BuildlessAutoDisposeNotifier<void> {
late final Chapter chapter;

View file

@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/colors.dart';
import 'package:mangayomi/utils/media_query.dart';
import 'package:numberpicker/numberpicker.dart';
class PlayerScreen extends ConsumerWidget {
const PlayerScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final markEpisodeAsSeenType = ref.watch(markEpisodeAsSeenTypeStateProvider);
final defaultSkipIntroLength =
ref.watch(defaultSkipIntroLengthStateProvider);
// final defaultDoubleTapToSkipLength =
// ref.watch(defaultDoubleTapToSkipLengthStateProvider);
final defaultPlayBackSpeed = ref.watch(defaultPlayBackSpeedStateProvider);
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.player),
),
body: SingleChildScrollView(
child: Column(
children: [
ListTile(
onTap: () {
final values = [100, 95, 90, 85, 80, 75, 70];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.markEpisodeAsSeenSetting),
content: SizedBox(
width: mediaWidth(context, 0.8),
child: ListView.builder(
shrinkWrap: true,
itemCount: values.length,
itemBuilder: (context, index) {
return RadioListTile(
dense: true,
contentPadding: const EdgeInsets.all(0),
value: values[index],
groupValue: markEpisodeAsSeenType,
onChanged: (value) {
ref
.read(markEpisodeAsSeenTypeStateProvider
.notifier)
.set(value!);
Navigator.pop(context);
},
title: Row(
children: [Text("${values[index]}%")],
),
);
},
)),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style:
TextStyle(color: primaryColor(context)),
)),
],
)
],
);
});
},
title: Text(context.l10n.markEpisodeAsSeenSetting),
subtitle: Text(
"$markEpisodeAsSeenType%",
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
ListTile(
onTap: () {
int currentIntValue = defaultSkipIntroLength;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.default_skip_intro_length),
content: StatefulBuilder(
builder: (context, setState) => SizedBox(
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NumberPicker(
value: currentIntValue,
minValue: 1,
maxValue: 255,
step: 1,
haptics: true,
textMapper: (numberText) => "${numberText}s",
onChanged: (value) =>
setState(() => currentIntValue = value),
),
],
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style:
TextStyle(color: primaryColor(context)),
)),
TextButton(
onPressed: () async {
ref
.read(
defaultSkipIntroLengthStateProvider
.notifier)
.set(currentIntValue);
Navigator.pop(context);
},
child: Text(
context.l10n.ok,
style:
TextStyle(color: primaryColor(context)),
)),
],
)
],
);
});
},
title: Text(context.l10n.default_skip_intro_length),
subtitle: Text(
"${defaultSkipIntroLength}s",
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
// ListTile(
// onTap: () {
// final values = [30, 20, 10, 5, 3, 0];
// showDialog(
// context: context,
// builder: (context) {
// return AlertDialog(
// title: Text("Default Double tap to skip length"),
// content: SizedBox(
// width: mediaWidth(context, 0.8),
// child: ListView.builder(
// shrinkWrap: true,
// itemCount: values.length,
// itemBuilder: (context, index) {
// return RadioListTile(
// dense: true,
// contentPadding: const EdgeInsets.all(0),
// value: values[index],
// groupValue: defaultDoubleTapToSkipLength,
// onChanged: (value) {
// ref
// .read(
// defaultDoubleTapToSkipLengthStateProvider
// .notifier)
// .set(value!);
// Navigator.pop(context);
// },
// title: Row(
// children: [Text("${values[index]}s")],
// ),
// );
// },
// )),
// actions: [
// Row(
// mainAxisAlignment: MainAxisAlignment.end,
// children: [
// TextButton(
// onPressed: () async {
// Navigator.pop(context);
// },
// child: Text(
// context.l10n.cancel,
// style:
// TextStyle(color: primaryColor(context)),
// )),
// ],
// )
// ],
// );
// });
// },
// title: Text("Default Double tap to skip length"),
// subtitle: Text(
// "${defaultDoubleTapToSkipLength}s",
// style: TextStyle(fontSize: 11, color: secondaryColor(context)),
// ),
// ),
ListTile(
onTap: () {
final values = [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.default_playback_speed_length),
content: SizedBox(
width: mediaWidth(context, 0.8),
child: ListView.builder(
shrinkWrap: true,
itemCount: values.length,
itemBuilder: (context, index) {
return RadioListTile(
dense: true,
contentPadding: const EdgeInsets.all(0),
value: values[index],
groupValue: defaultPlayBackSpeed,
onChanged: (value) {
ref
.read(defaultPlayBackSpeedStateProvider
.notifier)
.set(value!);
Navigator.pop(context);
},
title: Row(
children: [Text("x${values[index]}")],
),
);
},
)),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style:
TextStyle(color: primaryColor(context)),
)),
],
)
],
);
});
},
title: Text(context.l10n.default_playback_speed_length),
subtitle: Text(
"x$defaultPlayBackSpeed",
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,65 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'player_state_provider.g.dart';
@riverpod
class MarkEpisodeAsSeenTypeState extends _$MarkEpisodeAsSeenTypeState {
@override
int build() {
return isar.settings.getSync(227)!.markEpisodeAsSeenType ?? 75;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..markEpisodeAsSeenType = value));
}
}
@riverpod
class DefaultSkipIntroLengthState extends _$DefaultSkipIntroLengthState {
@override
int build() {
return isar.settings.getSync(227)!.defaultSkipIntroLength ?? 85;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..defaultSkipIntroLength = value));
}
}
@riverpod
class DefaultDoubleTapToSkipLengthState
extends _$DefaultDoubleTapToSkipLengthState {
@override
int build() {
return isar.settings.getSync(227)!.defaultDoubleTapToSkipLength ?? 10;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(() =>
isar.settings.putSync(settings!..defaultDoubleTapToSkipLength = value));
}
}
@riverpod
class DefaultPlayBackSpeedState extends _$DefaultPlayBackSpeedState {
@override
double build() {
return isar.settings.getSync(227)!.defaultPlayBackSpeed ?? 1.0;
}
void set(double value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..defaultPlayBackSpeed = value));
}
}

View file

@ -0,0 +1,78 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_state_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$markEpisodeAsSeenTypeStateHash() =>
r'94ae90e6bc51bbd4f88dfc9780cc5e9eb4ed5770';
/// See also [MarkEpisodeAsSeenTypeState].
@ProviderFor(MarkEpisodeAsSeenTypeState)
final markEpisodeAsSeenTypeStateProvider =
AutoDisposeNotifierProvider<MarkEpisodeAsSeenTypeState, int>.internal(
MarkEpisodeAsSeenTypeState.new,
name: r'markEpisodeAsSeenTypeStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$markEpisodeAsSeenTypeStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$MarkEpisodeAsSeenTypeState = AutoDisposeNotifier<int>;
String _$defaultSkipIntroLengthStateHash() =>
r'fee9c7dd76ad84a16c6ac380285f5cdfe43fc537';
/// See also [DefaultSkipIntroLengthState].
@ProviderFor(DefaultSkipIntroLengthState)
final defaultSkipIntroLengthStateProvider =
AutoDisposeNotifierProvider<DefaultSkipIntroLengthState, int>.internal(
DefaultSkipIntroLengthState.new,
name: r'defaultSkipIntroLengthStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$defaultSkipIntroLengthStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DefaultSkipIntroLengthState = AutoDisposeNotifier<int>;
String _$defaultDoubleTapToSkipLengthStateHash() =>
r'5f60e645c464503f06f992cba5b61fe81cc8d112';
/// See also [DefaultDoubleTapToSkipLengthState].
@ProviderFor(DefaultDoubleTapToSkipLengthState)
final defaultDoubleTapToSkipLengthStateProvider = AutoDisposeNotifierProvider<
DefaultDoubleTapToSkipLengthState, int>.internal(
DefaultDoubleTapToSkipLengthState.new,
name: r'defaultDoubleTapToSkipLengthStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$defaultDoubleTapToSkipLengthStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DefaultDoubleTapToSkipLengthState = AutoDisposeNotifier<int>;
String _$defaultPlayBackSpeedStateHash() =>
r'865d17020e99aad291633b0829e0b9b502356c71';
/// See also [DefaultPlayBackSpeedState].
@ProviderFor(DefaultPlayBackSpeedState)
final defaultPlayBackSpeedStateProvider =
AutoDisposeNotifierProvider<DefaultPlayBackSpeedState, double>.internal(
DefaultPlayBackSpeedState.new,
name: r'defaultPlayBackSpeedStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$defaultPlayBackSpeedStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DefaultPlayBackSpeedState = AutoDisposeNotifier<double>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -31,6 +31,11 @@ class SettingsScreen extends StatelessWidget {
subtitle: l10n.reader_subtitle,
icon: Icons.chrome_reader_mode_rounded,
onTap: () => context.push('/readerMode')),
ListTileWidget(
title: l10n.player,
subtitle: l10n.reader_subtitle,
icon: Icons.play_circle_outline_outlined,
onTap: () => context.push('/playerMode')),
ListTileWidget(
title: l10n.downloads,
subtitle: l10n.downloads_subtitle,

View file

@ -10,6 +10,7 @@ import 'package:mangayomi/modules/browse/sources/sources_filter_screen.dart';
import 'package:mangayomi/modules/more/backup_and_restore/backup_and_restore.dart';
import 'package:mangayomi/modules/more/categories/categories_screen.dart';
import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_screen.dart';
import 'package:mangayomi/modules/more/settings/track/track.dart';
import 'package:mangayomi/modules/more/settings/track/manage_trackers/manage_trackers.dart';
import 'package:mangayomi/modules/more/settings/track/manage_trackers/tracking_detail.dart';
@ -485,6 +486,19 @@ class RouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/playerMode",
name: "playerMode",
builder: (context, state) {
return const PlayerScreen();
},
pageBuilder: (context, state) {
return transitionPage(
key: state.pageKey,
child: const PlayerScreen(),
);
},
),
];
}

View file

@ -6,7 +6,7 @@ part of 'kitsu.dart';
// RiverpodGenerator
// **************************************************************************
String _$kitsuHash() => r'36f64dbeaabb1338240ff372079a6ecb166abccb';
String _$kitsuHash() => r'c6a31bcfb827bba5d0938d350538a01926d42297';
/// Copied from Dart SDK
class _SystemHash {