mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
737 lines
27 KiB
Dart
737 lines
27 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:isar_community/isar.dart';
|
|
import 'package:mangayomi/eval/model/m_manga.dart';
|
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
|
import 'package:mangayomi/main.dart';
|
|
import 'package:mangayomi/models/category.dart';
|
|
import 'package:mangayomi/models/changed.dart';
|
|
import 'package:mangayomi/models/chapter.dart';
|
|
import 'package:mangayomi/models/history.dart';
|
|
import 'package:mangayomi/models/manga.dart';
|
|
import 'package:mangayomi/models/track_search.dart';
|
|
import 'package:mangayomi/models/update.dart';
|
|
import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart';
|
|
import 'package:mangayomi/modules/manga/detail/providers/isar_providers.dart';
|
|
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
|
import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart';
|
|
import 'package:mangayomi/modules/manga/detail/widgets/chapter_filter_list_tile_widget.dart';
|
|
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
|
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
|
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
|
import 'package:mangayomi/models/source.dart';
|
|
import 'package:mangayomi/services/get_detail.dart';
|
|
import 'package:mangayomi/services/search_.dart';
|
|
import 'package:mangayomi/utils/cached_network.dart';
|
|
import 'package:mangayomi/utils/date.dart';
|
|
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
|
import 'package:mangayomi/utils/constant.dart';
|
|
import 'package:mangayomi/utils/headers.dart';
|
|
import 'package:mangayomi/utils/language.dart';
|
|
import 'package:mangayomi/modules/widgets/bottom_text_widget.dart';
|
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
|
|
|
class MigrationScreen extends ConsumerStatefulWidget {
|
|
final Manga manga;
|
|
final TrackSearch? trackSearch;
|
|
const MigrationScreen({required this.manga, this.trackSearch, super.key});
|
|
|
|
@override
|
|
ConsumerState<MigrationScreen> createState() => _MigrationScreenScreenState();
|
|
}
|
|
|
|
class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
|
String _query = "";
|
|
final _textEditingController = TextEditingController();
|
|
late final List<Source> sourceList =
|
|
ref.read(onlyIncludePinnedSourceStateProvider)
|
|
? isar.sources
|
|
.filter()
|
|
.isPinnedEqualTo(true)
|
|
.and()
|
|
.itemTypeEqualTo(widget.manga.itemType)
|
|
.findAllSync()
|
|
: isar.sources
|
|
.filter()
|
|
.idIsNotNull()
|
|
.and()
|
|
.isAddedEqualTo(true)
|
|
.and()
|
|
.itemTypeEqualTo(widget.manga.itemType)
|
|
.findAllSync();
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final query = widget.manga.name ?? widget.manga.author ?? "";
|
|
_textEditingController.text = query;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = l10nLocalizations(context)!;
|
|
final query = _query.isNotEmpty
|
|
? _query
|
|
: widget.manga.name ?? widget.manga.author ?? "";
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(
|
|
widget.trackSearch == null ? l10n.migrate : l10n.track_library_add,
|
|
),
|
|
leading: Container(),
|
|
actions: [
|
|
SeachFormTextField(
|
|
onChanged: (value) {},
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
onFieldSubmitted: (value) async {
|
|
if (!(_query == _textEditingController.text)) {
|
|
setState(() {
|
|
_query = "";
|
|
});
|
|
// Yield a frame so the empty state is rendered before re-querying
|
|
await WidgetsBinding.instance.endOfFrame;
|
|
setState(() {
|
|
_query = value;
|
|
});
|
|
}
|
|
},
|
|
onSuffixPressed: () {
|
|
_textEditingController.clear();
|
|
setState(() {
|
|
_query = "";
|
|
});
|
|
},
|
|
controller: _textEditingController,
|
|
autofocus: false,
|
|
),
|
|
],
|
|
),
|
|
body:
|
|
_query.isNotEmpty ||
|
|
(widget.manga.name != null && widget.manga.author != null)
|
|
? SuperListView.builder(
|
|
itemCount: sourceList.length,
|
|
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
|
|
itemBuilder: (context, index) {
|
|
final source = sourceList[index];
|
|
return SizedBox(
|
|
height: 260,
|
|
child: MigrationSourceSearchScreen(
|
|
key: ValueKey(query),
|
|
query: query,
|
|
manga: widget.manga,
|
|
source: source,
|
|
trackSearch: widget.trackSearch,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: Container(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_textEditingController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class MigrationSourceSearchScreen extends ConsumerStatefulWidget {
|
|
final String query;
|
|
final Manga manga;
|
|
final TrackSearch? trackSearch;
|
|
|
|
final Source source;
|
|
const MigrationSourceSearchScreen({
|
|
super.key,
|
|
required this.query,
|
|
required this.manga,
|
|
required this.source,
|
|
this.trackSearch,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<MigrationSourceSearchScreen> createState() =>
|
|
_MigrationSourceSearchScreenState();
|
|
}
|
|
|
|
class _MigrationSourceSearchScreenState
|
|
extends ConsumerState<MigrationSourceSearchScreen> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_init();
|
|
}
|
|
|
|
String _errorMessage = "";
|
|
bool _isLoading = true;
|
|
MPages? pages;
|
|
Future<void> _init() async {
|
|
try {
|
|
_errorMessage = "";
|
|
pages = await ref.read(
|
|
searchProvider(
|
|
source: widget.source,
|
|
page: 1,
|
|
query: widget.query,
|
|
filterList: [],
|
|
).future,
|
|
);
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_errorMessage = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = l10nLocalizations(context)!;
|
|
|
|
return Scaffold(
|
|
body: SizedBox(
|
|
height: 260,
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
dense: true,
|
|
title: Text(widget.source.name!),
|
|
subtitle: Text(
|
|
completeLanguageName(widget.source.lang!),
|
|
style: const TextStyle(fontSize: 10),
|
|
),
|
|
),
|
|
Flexible(
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: Builder(
|
|
builder: (context) {
|
|
if (_errorMessage.isNotEmpty) {
|
|
return Center(child: Text(_errorMessage));
|
|
}
|
|
if (pages!.list.isNotEmpty) {
|
|
return SuperListView.builder(
|
|
extentPrecalculationPolicy:
|
|
SuperPrecalculationPolicy(),
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: pages!.list.length,
|
|
itemBuilder: (context, index) {
|
|
return MigrationMangaGlobalImageCard(
|
|
oldManga: widget.manga,
|
|
manga: pages!.list[index],
|
|
source: widget.source,
|
|
trackSearch: widget.trackSearch,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
return Center(child: Text(l10n.no_result));
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MigrationMangaGlobalImageCard extends ConsumerStatefulWidget {
|
|
final Manga oldManga;
|
|
final MManga manga;
|
|
final Source source;
|
|
final TrackSearch? trackSearch;
|
|
|
|
const MigrationMangaGlobalImageCard({
|
|
super.key,
|
|
required this.oldManga,
|
|
required this.manga,
|
|
required this.source,
|
|
this.trackSearch,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<MigrationMangaGlobalImageCard> createState() =>
|
|
_MigrationMangaGlobalImageCardState();
|
|
}
|
|
|
|
class _MigrationMangaGlobalImageCardState
|
|
extends ConsumerState<MigrationMangaGlobalImageCard>
|
|
with AutomaticKeepAliveClientMixin<MigrationMangaGlobalImageCard> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
final l10n = l10nLocalizations(context)!;
|
|
final getMangaDetail = widget.manga;
|
|
return GestureDetector(
|
|
onTap: () => _showMigrateDialog(context, l10n),
|
|
child: StreamBuilder(
|
|
stream: isar.mangas
|
|
.filter()
|
|
.langEqualTo(widget.source.lang)
|
|
.nameEqualTo(getMangaDetail.name)
|
|
.sourceEqualTo(widget.source.name)
|
|
.watch(fireImmediately: true),
|
|
builder: (context, snapshot) {
|
|
final hasData = snapshot.hasData && snapshot.data!.isNotEmpty;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 10),
|
|
child: Stack(
|
|
children: [
|
|
SizedBox(
|
|
width: 110,
|
|
child: Column(
|
|
children: [
|
|
Builder(
|
|
builder: (context) {
|
|
if (hasData &&
|
|
snapshot.data!.first.customCoverImage != null) {
|
|
return Image.memory(
|
|
snapshot.data!.first.customCoverImage
|
|
as Uint8List,
|
|
);
|
|
}
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(5),
|
|
child: cachedNetworkImage(
|
|
headers: ref.watch(
|
|
headersProvider(
|
|
source: widget.source.name!,
|
|
lang: widget.source.lang!,
|
|
sourceId: widget.source.id,
|
|
),
|
|
),
|
|
imageUrl: toImgUrl(
|
|
hasData
|
|
? snapshot
|
|
.data!
|
|
.first
|
|
.customCoverFromTracker ??
|
|
snapshot.data!.first.imageUrl ??
|
|
""
|
|
: getMangaDetail.imageUrl ?? "",
|
|
),
|
|
width: 110,
|
|
height: 150,
|
|
fit: BoxFit.cover,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
BottomTextWidget(
|
|
fontSize: 12.0,
|
|
text: widget.manga.name!,
|
|
isLoading: true,
|
|
textColor: Theme.of(context).textTheme.bodyLarge!.color,
|
|
isComfortableGrid: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 110,
|
|
height: 150,
|
|
color: hasData && snapshot.data!.first.favorite!
|
|
? Colors.black.withValues(alpha: 0.7)
|
|
: null,
|
|
),
|
|
if (hasData && snapshot.data!.first.favorite!)
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: Icon(
|
|
Icons.collections_bookmark,
|
|
color: context.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
void _showMigrateDialog(BuildContext context, dynamic l10n) {
|
|
ref
|
|
.watch(
|
|
getDetailProvider(
|
|
url: widget.manga.link!,
|
|
source: widget.source,
|
|
).future,
|
|
)
|
|
.then((preview) {
|
|
if (context.mounted) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return AlertDialog(
|
|
title: Text(
|
|
widget.trackSearch == null
|
|
? l10n.migrate_confirm
|
|
: l10n.track_library_add_confirm,
|
|
),
|
|
content: preview.chapters != null
|
|
? SizedBox(
|
|
height: ctx.height(0.5),
|
|
width: ctx.width(1),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.all(0),
|
|
sliver: SuperSliverList.builder(
|
|
itemCount: preview.chapters!.length,
|
|
itemBuilder: (context, index) {
|
|
final chapter = preview.chapters![index];
|
|
return ListTile(
|
|
title: Row(
|
|
children: [
|
|
Flexible(
|
|
child: Text(
|
|
preview.chapters![index].name!,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
subtitle: Row(
|
|
children: [
|
|
Text(
|
|
chapter.dateUpload == null ||
|
|
chapter.dateUpload!.isEmpty
|
|
? ""
|
|
: dateFormat(
|
|
chapter.dateUpload!,
|
|
ref: ref,
|
|
context: context,
|
|
),
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
if (chapter.scanlator?.isNotEmpty ??
|
|
false)
|
|
Row(
|
|
children: [
|
|
const Text(' • '),
|
|
Text(
|
|
chapter.scanlator!,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: Text(l10n.n_chapters(0)),
|
|
actions: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: Text(l10n.cancel),
|
|
),
|
|
const SizedBox(width: 15),
|
|
Consumer(
|
|
builder: (context, ref, child) => TextButton(
|
|
onPressed: () async {
|
|
if (widget.trackSearch == null) {
|
|
await _migrateManga(preview);
|
|
if (ctx.mounted) {
|
|
Navigator.pop(ctx);
|
|
Navigator.pop(ctx);
|
|
}
|
|
} else {
|
|
await _addTrackManga(context);
|
|
}
|
|
},
|
|
child: Text(l10n.ok),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _addTrackManga(BuildContext context) async {
|
|
List<int> categoryIds = [];
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return StatefulBuilder(
|
|
builder: (context, setState) {
|
|
final l10n = l10nLocalizations(context)!;
|
|
return AlertDialog(
|
|
title: Text(l10n.set_categories),
|
|
content: SizedBox(
|
|
width: context.width(0.8),
|
|
child: StreamBuilder(
|
|
stream: isar.categorys
|
|
.filter()
|
|
.idIsNotNull()
|
|
.and()
|
|
.forItemTypeEqualTo(widget.oldManga.itemType)
|
|
.watch(fireImmediately: true),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
|
final entries = snapshot.data!;
|
|
return SuperListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: entries.length,
|
|
itemBuilder: (context, index) {
|
|
return ListTileChapterFilter(
|
|
label: entries[index].name!,
|
|
onTap: () {
|
|
setState(() {
|
|
if (categoryIds.contains(entries[index].id)) {
|
|
categoryIds.remove(entries[index].id);
|
|
} else {
|
|
categoryIds.add(entries[index].id!);
|
|
}
|
|
});
|
|
},
|
|
type: categoryIds.contains(entries[index].id)
|
|
? 1
|
|
: 0,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
return Container();
|
|
},
|
|
),
|
|
),
|
|
actions: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () {
|
|
context.push(
|
|
"/categories",
|
|
extra: (
|
|
true,
|
|
widget.oldManga.itemType == ItemType.manga
|
|
? 0
|
|
: widget.oldManga.itemType == ItemType.anime
|
|
? 1
|
|
: 2,
|
|
),
|
|
);
|
|
Navigator.pop(context);
|
|
},
|
|
child: Text(l10n.edit),
|
|
),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
child: Text(l10n.cancel),
|
|
),
|
|
const SizedBox(width: 15),
|
|
TextButton(
|
|
onPressed: () async {
|
|
final model = widget.manga;
|
|
final manga = Manga(
|
|
name: model.name,
|
|
artist: model.artist,
|
|
author: model.author,
|
|
description: model.description,
|
|
imageUrl: model.imageUrl,
|
|
link: model.link,
|
|
genre: model.genre,
|
|
status: model.status ?? Status.unknown,
|
|
source: widget.source.name,
|
|
lang: widget.source.lang,
|
|
itemType: widget.oldManga.itemType,
|
|
favorite: true,
|
|
categories: categoryIds,
|
|
dateAdded: DateTime.now().millisecondsSinceEpoch,
|
|
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
|
sourceId: widget.source.id,
|
|
);
|
|
int mangaId = -1;
|
|
isar.writeTxnSync(() {
|
|
mangaId = isar.mangas.putSync(manga);
|
|
});
|
|
if (mangaId != -1) {
|
|
await ref
|
|
.read(
|
|
trackStateProvider(
|
|
track: null,
|
|
itemType: widget.oldManga.itemType,
|
|
widgetRef: ref,
|
|
).notifier,
|
|
)
|
|
.setTrackSearch(
|
|
widget.trackSearch!,
|
|
mangaId,
|
|
widget.trackSearch!.syncId!,
|
|
);
|
|
}
|
|
if (context.mounted) {
|
|
Navigator.pop(context);
|
|
Navigator.pop(context);
|
|
}
|
|
},
|
|
child: Text(l10n.ok),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _migrateManga(MManga preview) async {
|
|
String? historyChapter;
|
|
String? historyDate;
|
|
List<Chapter> chaptersProgress = [];
|
|
isar.writeTxnSync(() {
|
|
final histories = isar.historys
|
|
.filter()
|
|
.mangaIdEqualTo(widget.oldManga.id)
|
|
.sortByDate()
|
|
.findAllSync();
|
|
historyChapter = _extractChapterNumber(
|
|
histories.lastOrNull?.chapter.value?.name ?? "",
|
|
);
|
|
historyDate = histories.lastOrNull?.date;
|
|
for (var history in histories) {
|
|
isar.historys.deleteSync(history.id!);
|
|
ref
|
|
.read(synchingProvider(syncId: 1).notifier)
|
|
.addChangedPart(ActionType.removeHistory, history.id, "{}", false);
|
|
}
|
|
for (var chapter in widget.oldManga.chapters) {
|
|
chaptersProgress.add(chapter);
|
|
isar.updates
|
|
.filter()
|
|
.mangaIdEqualTo(chapter.mangaId)
|
|
.chapterNameEqualTo(chapter.name)
|
|
.deleteAllSync();
|
|
isar.chapters.deleteSync(chapter.id!);
|
|
ref
|
|
.read(synchingProvider(syncId: 1).notifier)
|
|
.addChangedPart(ActionType.removeChapter, chapter.id, "{}", false);
|
|
}
|
|
widget.oldManga.name = widget.manga.name;
|
|
widget.oldManga.link = widget.manga.link;
|
|
widget.oldManga.imageUrl = widget.manga.imageUrl;
|
|
widget.oldManga.lang = widget.source.lang;
|
|
widget.oldManga.source = widget.source.name;
|
|
widget.oldManga.sourceId = widget.source.id;
|
|
widget.oldManga.artist = preview.artist;
|
|
widget.oldManga.author = preview.author;
|
|
widget.oldManga.status = preview.status ?? widget.oldManga.status;
|
|
widget.oldManga.description = preview.description;
|
|
widget.oldManga.genre = preview.genre;
|
|
widget.oldManga.updatedAt = DateTime.now().millisecondsSinceEpoch;
|
|
isar.mangas.putSync(widget.oldManga);
|
|
});
|
|
await ref.read(
|
|
updateMangaDetailProvider(
|
|
mangaId: widget.oldManga.id,
|
|
isInit: false,
|
|
).future,
|
|
);
|
|
isar.writeTxnSync(() {
|
|
for (var oldChapter in chaptersProgress) {
|
|
final chapter = isar.chapters
|
|
.filter()
|
|
.mangaIdEqualTo(widget.oldManga.id)
|
|
.nameContains(
|
|
_extractChapterNumber(oldChapter.name ?? "") ?? ".....",
|
|
caseSensitive: false,
|
|
)
|
|
.findFirstSync();
|
|
if (chapter != null) {
|
|
chapter.isBookmarked = oldChapter.isBookmarked;
|
|
chapter.lastPageRead = oldChapter.lastPageRead;
|
|
chapter.isRead = oldChapter.isRead;
|
|
isar.chapters.putSync(chapter);
|
|
}
|
|
}
|
|
final chapter = isar.chapters
|
|
.filter()
|
|
.mangaIdEqualTo(widget.oldManga.id)
|
|
.nameContains(historyChapter ?? ".....", caseSensitive: false)
|
|
.findFirstSync();
|
|
if (chapter != null) {
|
|
isar.historys.putSync(
|
|
History(
|
|
mangaId: widget.oldManga.id,
|
|
date:
|
|
historyDate ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
|
itemType: widget.oldManga.itemType,
|
|
chapterId: chapter.id,
|
|
)..chapter.value = chapter,
|
|
);
|
|
}
|
|
});
|
|
ref.invalidate(getMangaDetailStreamProvider(mangaId: widget.oldManga.id!));
|
|
}
|
|
|
|
String? _extractChapterNumber(String chapterName) {
|
|
return RegExp(
|
|
r'\s*(\d+\.\d+)\s*',
|
|
multiLine: true,
|
|
).firstMatch(chapterName)?.group(0) ??
|
|
RegExp(
|
|
r'\s*(\d+)\s*',
|
|
multiLine: true,
|
|
).firstMatch(chapterName)?.group(0);
|
|
}
|
|
}
|
|
|
|
class SuperPrecalculationPolicy extends ExtentPrecalculationPolicy {
|
|
@override
|
|
bool shouldPrecalculateExtents(ExtentPrecalculationContext context) {
|
|
return context.numberOfItems < 100;
|
|
}
|
|
}
|