mangayomi-mirror/lib/modules/library/library_screen.dart
2025-12-29 23:20:08 +01:00

2308 lines
94 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar_community/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/changed.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/modules/library/providers/add_torrent.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart';
import 'package:mangayomi/modules/more/providers/downloaded_only_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/widgets/bottom_select_bar.dart';
import 'package:mangayomi/modules/widgets/category_selection_dialog.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/services/library_updater.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart';
import 'package:mangayomi/modules/library/widgets/library_gridview_widget.dart';
import 'package:mangayomi/modules/library/widgets/library_listview_widget.dart';
import 'package:mangayomi/modules/manga/detail/widgets/chapter_filter_list_tile_widget.dart';
import 'package:mangayomi/modules/manga/detail/widgets/chapter_sort_list_tile_widget.dart';
import 'package:mangayomi/modules/widgets/error_text.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/global_style.dart';
import 'package:mangayomi/utils/item_type_localization.dart';
import 'package:path/path.dart' as p;
import 'package:riverpod_annotation/riverpod_annotation.dart';
class LibraryScreen extends ConsumerStatefulWidget {
final ItemType itemType;
final String? presetInput;
const LibraryScreen({
required this.itemType,
required this.presetInput,
super.key,
});
@override
ConsumerState<LibraryScreen> createState() => _LibraryScreenState();
}
class _LibraryScreenState extends ConsumerState<LibraryScreen>
with TickerProviderStateMixin {
bool _isSearch = false;
final List<Manga> _entries = [];
final _textEditingController = TextEditingController();
TabController? tabBarController;
int _tabIndex = 0;
@override
void initState() {
super.initState();
if (widget.presetInput != null) {
_isSearch = true;
_textEditingController.text = widget.presetInput!;
}
}
@override
void dispose() {
_textEditingController.dispose();
tabBarController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final settingsStream = ref.watch(getSettingsStreamProvider);
return settingsStream.when(
data: (settingsList) {
final settings = settingsList.first;
final categories = ref.watch(
getMangaCategorieStreamProvider(itemType: widget.itemType),
);
final withoutCategories = ref.watch(
getAllMangaWithoutCategoriesStreamProvider(itemType: widget.itemType),
);
final downloadedOnly = ref.watch(downloadedOnlyStateProvider);
final mangaAll = ref.watch(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
T watchWithSettings<T>(
ProviderListenable<T> Function({
required ItemType itemType,
required Settings settings,
})
providerFn,
) {
return ref.watch(
providerFn(itemType: widget.itemType, settings: settings),
);
}
T watchWithSettingsAndManga<T>(
ProviderListenable<T> Function({
required ItemType itemType,
required List<Manga> mangaList,
required Settings settings,
})
providerFn,
) {
return ref.watch(
providerFn(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
),
);
}
final showCategoryTabs = watchWithSettings(
libraryShowCategoryTabsStateProvider.call,
);
final l10n = l10nLocalizations(context)!;
return Scaffold(
body: mangaAll.when(
data: (man) {
return withoutCategories.when(
data: (withoutCategory) {
return categories.when(
data: (data) {
bool reverse = watchWithSettings(
sortLibraryMangaStateProvider.call,
).reverse!;
final continueReaderBtn = watchWithSettings(
libraryShowContinueReadingButtonStateProvider.call,
);
final showNumbersOfItems = watchWithSettings(
libraryShowNumbersOfItemsStateProvider.call,
);
final localSource = watchWithSettings(
libraryLocalSourceStateProvider.call,
);
final downloadedChapter = watchWithSettings(
libraryDownloadedChaptersStateProvider.call,
);
final language = watchWithSettings(
libraryLanguageStateProvider.call,
);
final displayType = watchWithSettings(
libraryDisplayTypeStateProvider.call,
);
final isNotFiltering = watchWithSettingsAndManga(
mangasFilterResultStateProvider.call,
);
final downloadFilterType = watchWithSettingsAndManga(
mangaFilterDownloadedStateProvider.call,
);
final unreadFilterType = watchWithSettingsAndManga(
mangaFilterUnreadStateProvider.call,
);
final startedFilterType = watchWithSettingsAndManga(
mangaFilterStartedStateProvider.call,
);
final bookmarkedFilterType = watchWithSettingsAndManga(
mangaFilterBookmarkedStateProvider.call,
);
final sortType =
watchWithSettings(
sortLibraryMangaStateProvider.call,
).index
as int;
if (data.isNotEmpty && showCategoryTabs) {
data.sort((a, b) => (a.pos ?? 0).compareTo(b.pos ?? 0));
final entr = data
.where((e) => !(e.hide ?? false))
.toList();
int tabCount = withoutCategory.isNotEmpty
? entr.length + 1
: entr.length;
if (tabCount <= 0) {
return _bodyWithoutCategories(
withoutCategories: true,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
reverse: reverse,
downloadedChapter: downloadedChapter,
continueReaderBtn: continueReaderBtn,
language: language,
displayType: displayType,
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly: downloadedOnly,
);
}
if (tabCount > 0 &&
(tabBarController == null ||
tabBarController!.length != tabCount)) {
int newTabIndex = _tabIndex;
if (newTabIndex >= tabCount) {
newTabIndex = tabCount - 1;
}
tabBarController?.dispose();
tabBarController = TabController(
length: tabCount,
vsync: this,
initialIndex: newTabIndex,
);
_tabIndex = newTabIndex;
tabBarController!.addListener(() {
setState(() {
_tabIndex = tabBarController!.index;
});
});
}
return Consumer(
builder: (context, ref, child) {
final numberOfItemsList = _filterAndSortManga(
data: man,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
);
final withoutCategoryNumberOfItemsList =
_filterAndSortManga(
data: withoutCategory,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
);
return DefaultTabController(
length: entr.length,
child: Scaffold(
appBar: _appBar(
isNotFiltering,
showNumbersOfItems,
numberOfItemsList.length,
ref,
[],
true,
withoutCategory.isNotEmpty && _tabIndex == 0
? null
: entr[withoutCategory.isNotEmpty
? _tabIndex - 1
: _tabIndex]
.id!,
settings,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
isScrollable: true,
controller: tabBarController,
tabs: [
if (withoutCategory.isNotEmpty)
for (
var i = 0;
i < entr.length + 1;
i++
)
Row(
children: [
Tab(
text: i == 0
? l10n.default0
: entr[i - 1].name,
),
const SizedBox(width: 4),
if (showNumbersOfItems)
i == 0
? CircleAvatar(
backgroundColor:
Theme.of(
context,
).focusColor,
radius: 8,
child: Text(
withoutCategoryNumberOfItemsList
.length
.toString(),
style: TextStyle(
fontSize: 10,
color:
Theme.of(
context,
)
.textTheme
.bodySmall!
.color,
),
),
)
: _categoriesNumberOfItems(
downloadFilterType:
downloadFilterType,
unreadFilterType:
unreadFilterType,
startedFilterType:
startedFilterType,
bookmarkedFilterType:
bookmarkedFilterType,
reverse: reverse,
downloadedChapter:
downloadedChapter,
continueReaderBtn:
continueReaderBtn,
categoryId:
entr[i - 1].id!,
settings: settings,
downloadedOnly:
downloadedOnly,
),
],
),
if (withoutCategory.isEmpty)
for (var i = 0; i < entr.length; i++)
Row(
children: [
Tab(text: entr[i].name),
const SizedBox(width: 4),
if (showNumbersOfItems)
_categoriesNumberOfItems(
downloadFilterType:
downloadFilterType,
unreadFilterType:
unreadFilterType,
startedFilterType:
startedFilterType,
bookmarkedFilterType:
bookmarkedFilterType,
reverse: reverse,
downloadedChapter:
downloadedChapter,
continueReaderBtn:
continueReaderBtn,
categoryId: entr[i].id!,
settings: settings,
downloadedOnly:
downloadedOnly,
),
],
),
],
),
Flexible(
child: TabBarView(
controller: tabBarController,
children: [
if (withoutCategory.isNotEmpty)
for (
var i = 0;
i < entr.length + 1;
i++
)
i == 0
? _bodyWithoutCategories(
withoutCategories: true,
downloadFilterType:
downloadFilterType,
unreadFilterType:
unreadFilterType,
startedFilterType:
startedFilterType,
bookmarkedFilterType:
bookmarkedFilterType,
reverse: reverse,
downloadedChapter:
downloadedChapter,
continueReaderBtn:
continueReaderBtn,
language: language,
displayType: displayType,
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly:
downloadedOnly,
)
: _bodyWithCatories(
categoryId:
entr[i - 1].id!,
downloadFilterType:
downloadFilterType,
unreadFilterType:
unreadFilterType,
startedFilterType:
startedFilterType,
bookmarkedFilterType:
bookmarkedFilterType,
reverse: reverse,
downloadedChapter:
downloadedChapter,
continueReaderBtn:
continueReaderBtn,
language: language,
displayType: displayType,
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly:
downloadedOnly,
),
if (withoutCategory.isEmpty)
for (
var i = 0;
i < entr.length;
i++
)
_bodyWithCatories(
categoryId: entr[i].id!,
downloadFilterType:
downloadFilterType,
unreadFilterType:
unreadFilterType,
startedFilterType:
startedFilterType,
bookmarkedFilterType:
bookmarkedFilterType,
reverse: reverse,
downloadedChapter:
downloadedChapter,
continueReaderBtn:
continueReaderBtn,
language: language,
displayType: displayType,
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly: downloadedOnly,
),
],
),
),
],
),
),
);
},
);
}
return Consumer(
builder: (context, ref, child) {
final numberOfItemsList = _filterAndSortManga(
data: man,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
);
return Scaffold(
appBar: _appBar(
isNotFiltering,
showNumbersOfItems,
numberOfItemsList.length,
ref,
numberOfItemsList,
false,
null,
settings,
),
body: _bodyWithoutCategories(
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
reverse: reverse,
downloadedChapter: downloadedChapter,
continueReaderBtn: continueReaderBtn,
language: language,
displayType: displayType,
ref: ref,
localSource: localSource,
settings: settings,
downloadedOnly: downloadedOnly,
),
);
},
);
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
);
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
);
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
),
bottomNavigationBar: Builder(
builder: (context) {
final mangaIds = ref.watch(mangasListStateProvider);
final color = Theme.of(context).textTheme.bodyLarge!.color!;
return BottomSelectBar(
isVisible: ref.watch(isLongPressedStateProvider),
actions: [
BottomSelectButton(
icon: Icon(Icons.label_outline_rounded, color: color),
onPressed: () {
final mangaIdsList = ref.watch(mangasListStateProvider);
final List<Manga> bulkMangas = mangaIdsList
.map((id) => isar.mangas.getSync(id)!)
.toList();
showCategorySelectionDialog(
context: context,
ref: ref,
itemType: widget.itemType,
bulkMangas: bulkMangas,
);
},
),
BottomSelectButton(
icon: Icon(Icons.done_all_sharp, color: color),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: true,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
},
),
BottomSelectButton(
icon: Icon(Icons.remove_done_sharp, color: color),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: false,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
},
),
// BottomBarAction(
// icon: Icon(Icons.download_outlined, color: color),
// onPressed: () {}
// ),
BottomSelectButton(
icon: Icon(Icons.delete_outline_outlined, color: color),
onPressed: () => _deleteManga(),
),
],
);
},
),
);
},
error: (error, e) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
);
}
Widget _categoriesNumberOfItems({
required int downloadFilterType,
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required bool reverse,
required bool downloadedChapter,
required bool continueReaderBtn,
required int categoryId,
required Settings settings,
required bool downloadedOnly,
}) {
final mangas = ref.watch(
getAllMangaStreamProvider(
categoryId: categoryId,
itemType: widget.itemType,
),
);
final sortType = ref
.watch(
sortLibraryMangaStateProvider(
itemType: widget.itemType,
settings: settings,
),
)
.index;
return mangas.when(
data: (data) {
final categoriNumberOfItemsList = _filterAndSortManga(
data: data,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType!,
downloadedOnly: downloadedOnly,
);
return CircleAvatar(
backgroundColor: Theme.of(context).focusColor,
radius: 8,
child: Text(
categoriNumberOfItemsList.length.toString(),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).textTheme.bodySmall!.color,
),
),
);
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
);
}
Widget _bodyWithCatories({
required int categoryId,
required int downloadFilterType,
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required bool reverse,
required bool downloadedChapter,
required bool continueReaderBtn,
required bool localSource,
required bool language,
required WidgetRef ref,
required DisplayType displayType,
required Settings settings,
required bool downloadedOnly,
}) {
final l10n = l10nLocalizations(context)!;
final mangas = ref.watch(
getAllMangaStreamProvider(
categoryId: categoryId,
itemType: widget.itemType,
),
);
final sortType = ref
.watch(
sortLibraryMangaStateProvider(
itemType: widget.itemType,
settings: settings,
),
)
.index;
final mangaIdsList = ref.watch(mangasListStateProvider);
return Scaffold(
body: mangas.when(
data: (data) {
final entries = _filterAndSortManga(
data: data,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType!,
downloadedOnly: downloadedOnly,
);
if (entries.isNotEmpty) {
final entriesManga = reverse ? entries.reversed.toList() : entries;
return RefreshIndicator(
onRefresh: () async {
await updateLibrary(
ref: ref,
context: context,
mangaList: data,
itemType: widget.itemType,
);
},
child: displayType == DisplayType.list
? LibraryListViewWidget(
entriesManga: entriesManga,
continueReaderBtn: continueReaderBtn,
downloadedChapter: downloadedChapter,
language: language,
mangaIdsList: mangaIdsList,
localSource: localSource,
)
: LibraryGridViewWidget(
entriesManga: entriesManga,
isCoverOnlyGrid:
!(displayType == DisplayType.compactGrid),
isComfortableGrid:
displayType == DisplayType.comfortableGrid,
continueReaderBtn: continueReaderBtn,
downloadedChapter: downloadedChapter,
language: language,
mangaIdsList: mangaIdsList,
localSource: localSource,
itemType: widget.itemType,
),
);
}
return Center(child: Text(l10n.empty_library));
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
),
);
}
Widget _bodyWithoutCategories({
required int downloadFilterType,
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required bool reverse,
required bool downloadedChapter,
required bool continueReaderBtn,
required bool localSource,
required bool language,
required DisplayType displayType,
required WidgetRef ref,
bool withoutCategories = false,
required Settings settings,
required bool downloadedOnly,
}) {
final sortType = ref
.watch(
sortLibraryMangaStateProvider(
itemType: widget.itemType,
settings: settings,
),
)
.index;
final manga = withoutCategories
? ref.watch(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
)
: ref.watch(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
final mangaIdsList = ref.watch(mangasListStateProvider);
final l10n = l10nLocalizations(context)!;
return manga.when(
data: (data) {
final entries = _filterAndSortManga(
data: data,
downloadFilterType: downloadFilterType,
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
sortType: sortType ?? 0,
downloadedOnly: downloadedOnly,
);
if (entries.isNotEmpty) {
final entriesManga = reverse ? entries.reversed.toList() : entries;
return RefreshIndicator(
onRefresh: () async {
await updateLibrary(
ref: ref,
context: context,
mangaList: data,
itemType: widget.itemType,
);
},
child: displayType == DisplayType.list
? LibraryListViewWidget(
entriesManga: entriesManga,
continueReaderBtn: continueReaderBtn,
downloadedChapter: downloadedChapter,
language: language,
mangaIdsList: mangaIdsList,
localSource: localSource,
)
: LibraryGridViewWidget(
entriesManga: entriesManga,
isCoverOnlyGrid: !(displayType == DisplayType.compactGrid),
isComfortableGrid:
displayType == DisplayType.comfortableGrid,
continueReaderBtn: continueReaderBtn,
downloadedChapter: downloadedChapter,
language: language,
mangaIdsList: mangaIdsList,
localSource: localSource,
itemType: widget.itemType,
),
);
}
return Center(child: Text(l10n.empty_library));
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
);
}
bool matchesSearchQuery(Manga manga, String query) {
final keywords = query
.toLowerCase()
.split(',')
.map((k) => k.trim())
.where((k) => k.isNotEmpty);
return keywords.any(
(keyword) =>
(manga.name?.toLowerCase().contains(keyword) ?? false) ||
(manga.source?.toLowerCase().contains(keyword) ?? false) ||
(manga.genre?.any((g) => g.toLowerCase().contains(keyword)) ?? false),
);
}
List<Manga> _filterAndSortManga({
required List<Manga> data,
required int downloadFilterType,
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required int sortType,
bool downloadedOnly = false,
}) {
List<Manga>? mangas;
final searchQuery = _textEditingController.text;
// Skip all filters, just do search
if (searchQuery.isNotEmpty && _ignoreFiltersOnSearch) {
mangas = data
.where((element) => matchesSearchQuery(element, searchQuery))
.toList();
} else {
// Apply filters + search
mangas = data
.where((element) {
// Filter by download
List list = [];
if (downloadFilterType == 1 || downloadedOnly) {
for (var chap in element.chapters) {
final modelChapDownload = isar.downloads
.filter()
.idEqualTo(chap.id)
.findAllSync();
if (modelChapDownload.isNotEmpty &&
modelChapDownload.first.isDownload == true) {
list.add(true);
}
}
return list.isNotEmpty;
} else if (downloadFilterType == 2) {
for (var chap in element.chapters) {
final modelChapDownload = isar.downloads
.filter()
.idEqualTo(chap.id)
.findAllSync();
if (!(modelChapDownload.isNotEmpty &&
modelChapDownload.first.isDownload == true)) {
list.add(true);
}
}
return list.length == element.chapters.length;
}
return true;
})
.where((element) {
// Filter by unread or started
List list = [];
if (unreadFilterType == 1 || startedFilterType == 1) {
for (var chap in element.chapters) {
if (!chap.isRead!) {
list.add(true);
}
}
return list.isNotEmpty;
} else if (unreadFilterType == 2 || startedFilterType == 2) {
List list = [];
for (var chap in element.chapters) {
if (chap.isRead!) {
list.add(true);
}
}
return list.length == element.chapters.length;
}
return true;
})
.where((element) {
// Filter by bookmarked
List list = [];
if (bookmarkedFilterType == 1) {
for (var chap in element.chapters) {
if (chap.isBookmarked!) {
list.add(true);
}
}
return list.isNotEmpty;
} else if (bookmarkedFilterType == 2) {
List list = [];
for (var chap in element.chapters) {
if (!chap.isBookmarked!) {
list.add(true);
}
}
return list.length == element.chapters.length;
}
return true;
})
.where(
(element) => searchQuery.isNotEmpty
? matchesSearchQuery(element, searchQuery)
: true,
)
.toList();
}
// Sorting the data based on selected sort type
mangas.sort((a, b) {
switch (sortType) {
case 0:
return a.name!.compareTo(b.name!);
case 1:
return a.lastRead!.compareTo(b.lastRead!);
case 2:
return a.lastUpdate?.compareTo(b.lastUpdate ?? 0) ?? 0;
case 3:
return a.chapters
.where((e) => !e.isRead!)
.length
.compareTo(b.chapters.where((e) => !e.isRead!).length);
case 4:
return a.chapters.length.compareTo(b.chapters.length);
case 5:
return (a.chapters.lastOrNull?.dateUpload ?? "").compareTo(
b.chapters.lastOrNull?.dateUpload ?? "",
);
case 6:
return a.dateAdded?.compareTo(b.dateAdded ?? 0) ?? 0;
default:
return 0;
}
});
return mangas;
}
void _deleteManga() {
List<int> fromLibList = [];
List<int> downloadedChapsList = [];
showDialog(
context: context,
builder: (context) {
return Consumer(
builder: (context, ref, child) {
final mangaIdsList = ref.watch(mangasListStateProvider);
final l10n = l10nLocalizations(context)!;
final List<Manga> mangasList = [];
for (var id in mangaIdsList) {
mangasList.add(isar.mangas.getSync(id)!);
}
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(l10n.remove),
content: SizedBox(
height: 100,
width: context.width(0.8),
child: Column(
children: [
ListTileChapterFilter(
label: l10n.from_library,
onTap: () {
setState(() {
if (fromLibList == mangaIdsList) {
fromLibList = [];
} else {
fromLibList = mangaIdsList;
}
});
},
type: fromLibList.isNotEmpty ? 1 : 0,
),
ListTileChapterFilter(
label: widget.itemType != ItemType.anime
? l10n.downloaded_chapters
: l10n.downloaded_episodes,
onTap: () {
setState(() {
if (downloadedChapsList == mangaIdsList) {
downloadedChapsList = [];
} else {
downloadedChapsList = mangaIdsList;
}
});
},
type: downloadedChapsList.isNotEmpty ? 1 : 0,
),
],
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
// From Library
if (fromLibList.isNotEmpty) {
isar.writeTxnSync(() {
for (var manga in mangasList) {
if (manga.isLocalArchive ?? false) {
_removeImport(ref, manga);
} else {
manga.favorite = false;
manga.updatedAt =
DateTime.now().millisecondsSinceEpoch;
isar.mangas.putSync(manga);
}
}
});
}
// Downloaded Chapters
if (downloadedChapsList.isNotEmpty) {
for (var manga in mangasList) {
if (!(manga.isLocalArchive ?? false)) {
String mangaDirectory = "";
if (manga.isLocalArchive ?? false) {
mangaDirectory = _deleteImport(
manga,
mangaDirectory,
);
// Also remove item from library
// else it has 0 chapters/episodes
// and when opened, shows exception
// "Null check operator"
isar.writeTxnSync(() {
_removeImport(ref, manga);
});
} else {
mangaDirectory = await _deleteDownload(
manga,
mangaDirectory,
);
}
if (mangaDirectory.isNotEmpty) {
final path = Directory(mangaDirectory);
if (path.existsSync() &&
path.listSync().isEmpty) {
path.deleteSync(recursive: true);
}
}
}
}
}
ref.read(mangasListStateProvider.notifier).clear();
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
if (mounted) {
Navigator.pop(context);
}
},
child: Text(l10n.ok),
),
],
),
],
);
},
);
},
);
},
);
}
/// helper method to remove the library entry of an imported item
/// does not remove from the device itself.
void _removeImport(WidgetRef ref, Manga manga) {
final provider = ref.read(synchingProvider(syncId: 1).notifier);
final histories = isar.historys
.filter()
.mangaIdEqualTo(manga.id)
.findAllSync();
for (var history in histories) {
isar.historys.deleteSync(history.id!);
provider.addChangedPart(
ActionType.removeHistory,
history.id,
"{}",
false,
);
}
for (var chapter in manga.chapters) {
final updates = isar.updates
.filter()
.mangaIdEqualTo(chapter.mangaId)
.chapterNameEqualTo(chapter.name)
.findAllSync();
for (var update in updates) {
isar.updates.deleteSync(update.id!);
provider.addChangedPart(
ActionType.removeUpdate,
update.id,
"{}",
false,
);
}
isar.chapters.deleteSync(chapter.id!);
provider.addChangedPart(
ActionType.removeChapter,
chapter.id,
"{}",
false,
);
}
isar.mangas.deleteSync(manga.id!);
provider.addChangedPart(ActionType.removeItem, manga.id, "{}", false);
}
/// helper method to delete imported mangas/animes
String _deleteImport(Manga manga, String mangaDirectory) {
for (var chapter in manga.chapters) {
final path = chapter.archivePath;
if (path == null) continue;
final chapterFile = File(path);
if (mangaDirectory.isEmpty) {
mangaDirectory = p.dirname(path);
}
try {
if (chapterFile.existsSync()) {
chapterFile.deleteSync();
}
} catch (_) {}
}
return mangaDirectory;
}
/// helper method to delete downloaded mangas/animes
Future<String> _deleteDownload(Manga manga, String mangaDirectory) async {
final storageProvider = StorageProvider();
Directory? mangaDir;
final idsToDelete = <int>{};
final downloadedIds = (await isar.downloads.where().idProperty().findAll())
.toSet();
if (downloadedIds.isEmpty) return mangaDirectory;
for (var chapter in manga.chapters) {
if (chapter.id == null || !downloadedIds.contains(chapter.id)) continue;
mangaDir ??= await storageProvider.getMangaMainDirectory(chapter);
final chapterDir = await storageProvider.getMangaChapterDirectory(
chapter,
mangaMainDirectory: mangaDir,
);
File? file;
if (mangaDirectory.isEmpty) mangaDirectory = mangaDir!.path;
if (manga.itemType == ItemType.manga) {
// ref: download_page_widget.dart
file = File(p.join(mangaDir!.path, "${chapter.name}.cbz"));
} else if (manga.itemType == ItemType.anime) {
// ref: download_page_widget.dart
file = File(
p.join(
mangaDir!.path,
"${chapter.name!.replaceForbiddenCharacters(' ')}.mp4",
),
);
}
try {
if (file != null && file.existsSync()) {
file.deleteSync();
}
if (chapterDir!.existsSync()) {
chapterDir.deleteSync(recursive: true);
}
} catch (_) {}
idsToDelete.add(chapter.id!);
}
if (idsToDelete.isNotEmpty) {
isar.writeTxnSync(() {
isar.downloads.deleteAllSync(idsToDelete.toList());
});
}
return mangaDirectory;
}
void _showDraggableMenu(Settings settings) {
final l10n = l10nLocalizations(context)!;
customDraggableTabBar(
tabs: [
Tab(text: l10n.filter),
Tab(text: l10n.sort),
Tab(text: l10n.display),
],
children: [
Consumer(
builder: (context, ref, chil) {
return Column(
children: [
ListTileChapterFilter(
label: l10n.downloaded,
type: ref.watch(
mangaFilterDownloadedStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
),
),
onTap: () {
ref
.read(
mangaFilterDownloadedStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
).notifier,
)
.update();
},
),
ListTileChapterFilter(
label: widget.itemType != ItemType.anime
? l10n.unread
: l10n.unwatched,
type: ref.watch(
mangaFilterUnreadStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
),
),
onTap: () {
ref
.read(
mangaFilterUnreadStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
).notifier,
)
.update();
},
),
ListTileChapterFilter(
label: l10n.started,
type: ref.watch(
mangaFilterStartedStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
),
),
onTap: () {
ref
.read(
mangaFilterStartedStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
).notifier,
)
.update();
},
),
ListTileChapterFilter(
label: l10n.bookmarked,
type: ref.watch(
mangaFilterBookmarkedStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
),
),
onTap: () {
setState(() {
ref
.read(
mangaFilterBookmarkedStateProvider(
itemType: widget.itemType,
mangaList: _entries,
settings: settings,
).notifier,
)
.update();
});
},
),
],
);
},
),
Consumer(
builder: (context, ref, chil) {
final reverse = ref
.read(
sortLibraryMangaStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.isReverse();
final reverseChapter = ref.watch(
sortLibraryMangaStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
return Column(
children: [
for (var i = 0; i < 7; i++)
ListTileChapterSort(
label: _getSortNameByIndex(i, context),
reverse: reverse,
onTap: () {
ref
.read(
sortLibraryMangaStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(i);
},
showLeading: reverseChapter.index == i,
),
],
);
},
),
Consumer(
builder: (context, ref, chil) {
final display = ref.watch(
libraryDisplayTypeStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
final displayV = ref.read(
libraryDisplayTypeStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
);
final showCategoryTabs = ref.watch(
libraryShowCategoryTabsStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
final continueReaderBtn = ref.watch(
libraryShowContinueReadingButtonStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
final showNumbersOfItems = ref.watch(
libraryShowNumbersOfItemsStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
final downloadedChapter = ref.watch(
libraryDownloadedChaptersStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
final language = ref.watch(
libraryLanguageStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
final localSource = ref.watch(
libraryLocalSourceStateProvider(
itemType: widget.itemType,
settings: settings,
),
);
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 10,
),
child: Row(children: [Text(l10n.display_mode)]),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 5,
horizontal: 20,
),
child: Wrap(
children: DisplayType.values.map(
(e) {
final selected = e == display;
return Padding(
padding: const EdgeInsets.only(right: 5),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
side: selected
? null
: BorderSide(
color: context.isLight
? Colors.black
: Colors.white,
width: 0.8,
),
shadowColor: Colors.transparent,
elevation: 0,
backgroundColor: selected
? context.primaryColor.withValues(
alpha: 0.2,
)
: Colors.transparent,
),
onPressed: () {
displayV.setLibraryDisplayType(e);
},
child: Text(
displayV.getLibraryDisplayTypeName(e, context),
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color,
fontSize: 14,
),
),
),
);
},
// RadioListTile<
// DisplayType>(
// dense: true,
// title: ,
// value: e,
// groupValue: displayV
// .getLibraryDisplayTypeValue(
// display),
// selected: true,
// onChanged: (value) {
// displayV
// .setLibraryDisplayType(
// value!);
// },
// ),
).toList(),
),
),
Consumer(
builder: (context, ref, child) {
final gridSize =
ref.watch(
libraryGridSizeStateProvider(
itemType: widget.itemType,
),
) ??
0;
return Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
top: 10,
),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: Column(
children: [
Text(context.l10n.grid_size),
Text(
gridSize == 0
? context.l10n.default0
: context.l10n.n_per_row(
gridSize.toString(),
),
),
],
),
),
Flexible(
flex: 7,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 5.0,
),
),
child: Slider(
min: 0.0,
max: 7,
divisions: max(7, 0),
value: gridSize.toDouble(),
onChanged: (value) {
HapticFeedback.vibrate();
ref
.read(
libraryGridSizeStateProvider(
itemType: widget.itemType,
).notifier,
)
.set(value.toInt());
},
onChangeEnd: (value) {
ref
.read(
libraryGridSizeStateProvider(
itemType: widget.itemType,
).notifier,
)
.set(value.toInt(), end: true);
},
),
),
),
],
),
);
},
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 10,
),
child: Row(children: [Text(l10n.badges)]),
),
Padding(
padding: const EdgeInsets.only(top: 5),
child: Column(
children: [
ListTileChapterFilter(
label: widget.itemType != ItemType.anime
? l10n.downloaded_chapters
: l10n.downloaded_episodes,
type: downloadedChapter ? 1 : 0,
onTap: () {
ref
.read(
libraryDownloadedChaptersStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(!downloadedChapter);
},
),
ListTileChapterFilter(
label: l10n.language,
type: language ? 1 : 0,
onTap: () {
ref
.read(
libraryLanguageStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(!language);
},
),
ListTileChapterFilter(
label: l10n.local_source,
type: localSource ? 1 : 0,
onTap: () {
ref
.read(
libraryLocalSourceStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(!localSource);
},
),
ListTileChapterFilter(
label: widget.itemType != ItemType.anime
? l10n.show_continue_reading_buttons
: l10n.show_continue_watching_buttons,
type: continueReaderBtn ? 1 : 0,
onTap: () {
ref
.read(
libraryShowContinueReadingButtonStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(!continueReaderBtn);
},
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 10,
),
child: Row(children: [Text(l10n.tabs)]),
),
Padding(
padding: const EdgeInsets.only(top: 5),
child: Column(
children: [
ListTileChapterFilter(
label: l10n.show_category_tabs,
type: showCategoryTabs ? 1 : 0,
onTap: () {
ref
.read(
libraryShowCategoryTabsStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(!showCategoryTabs);
},
),
ListTileChapterFilter(
label: l10n.show_numbers_of_items,
type: showNumbersOfItems ? 1 : 0,
onTap: () {
ref
.read(
libraryShowNumbersOfItemsStateProvider(
itemType: widget.itemType,
settings: settings,
).notifier,
)
.set(!showNumbersOfItems);
},
),
],
),
),
],
),
);
},
),
],
context: context,
vsync: this,
);
}
String _getSortNameByIndex(int index, BuildContext context) {
final l10n = l10nLocalizations(context)!;
if (index == 0) {
return l10n.alphabetically;
} else if (index == 1) {
return widget.itemType != ItemType.anime
? l10n.last_read
: l10n.last_watched;
} else if (index == 2) {
return l10n.last_update_check;
} else if (index == 3) {
return widget.itemType != ItemType.anime
? l10n.unread_count
: l10n.unwatched_count;
} else if (index == 4) {
return widget.itemType != ItemType.anime
? l10n.total_chapters
: l10n.total_episodes;
} else if (index == 5) {
return widget.itemType != ItemType.anime
? l10n.latest_chapter
: l10n.latest_episode;
}
return l10n.date_added;
}
bool _ignoreFiltersOnSearch = false;
final bool _isMobile = Platform.isIOS || Platform.isAndroid;
PreferredSize _appBar(
bool isNotFiltering,
bool showNumbersOfItems,
int numberOfItems,
WidgetRef ref,
List<Manga> mangas,
bool isCategory,
int? categoryId,
Settings settings,
) {
final isLongPressed = ref.watch(isLongPressedStateProvider);
final mangaIdsList = ref.watch(mangasListStateProvider);
final manga = categoryId == null
? ref.watch(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
)
: ref.watch(
getAllMangaStreamProvider(
categoryId: categoryId,
itemType: widget.itemType,
),
);
final l10n = l10nLocalizations(context)!;
return PreferredSize(
preferredSize: Size.fromHeight(AppBar().preferredSize.height),
child: isLongPressed
? manga.when(
data: (data) => Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: AppBar(
title: Text(mangaIdsList.length.toString()),
backgroundColor: context.primaryColor.withValues(alpha: 0.2),
leading: IconButton(
onPressed: () {
ref.read(mangasListStateProvider.notifier).clear();
ref
.read(isLongPressedStateProvider.notifier)
.update(!isLongPressed);
},
icon: const Icon(Icons.clear),
),
actions: [
IconButton(
onPressed: () {
for (var manga in data) {
ref
.read(mangasListStateProvider.notifier)
.selectAll(manga);
}
},
icon: const Icon(Icons.select_all),
),
IconButton(
onPressed: () {
if (data.length == mangaIdsList.length) {
for (var manga in data) {
ref
.read(mangasListStateProvider.notifier)
.selectSome(manga);
}
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
} else {
for (var manga in data) {
ref
.read(mangasListStateProvider.notifier)
.selectSome(manga);
}
}
},
icon: const Icon(Icons.flip_to_back_rounded),
),
],
),
),
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
)
: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: _isSearch
? null
: Row(
children: [
Text(
widget.itemType.localized(l10n),
style: TextStyle(color: Theme.of(context).hintColor),
),
const SizedBox(width: 10),
if (showNumbersOfItems)
Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Badge(
backgroundColor: Theme.of(context).focusColor,
label: Text(
numberOfItems.toString(),
style: TextStyle(
fontSize: 12,
color: Theme.of(
context,
).textTheme.bodySmall!.color,
),
),
),
),
],
),
actions: [
_isSearch
? SeachFormTextField(
onChanged: (value) {
setState(() {});
},
onPressed: () {
setState(() {
_isSearch = false;
});
_textEditingController.clear();
},
controller: _textEditingController,
onSuffixPressed: () {
_textEditingController.clear();
setState(() {});
},
)
: IconButton(
splashRadius: 20,
onPressed: () {
setState(() {
_isSearch = true;
});
_textEditingController.clear();
},
icon: const Icon(Icons.search),
),
// Checkbox when searching library to ignore filters
if (_isSearch)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_isMobile
// Adds a line break where spaces exist for better mobile layout.
// Works for languages that use spaces between words.
? l10n.ignore_filters.replaceFirst(' ', '\n')
// Removes manually added line breaks for Thai and Chinese,
// where spaces arent used, to ensure proper desktop rendering.
: l10n.ignore_filters.replaceAll('\n', ''),
textAlign: TextAlign.center,
),
Checkbox(
value: _ignoreFiltersOnSearch,
onChanged: (val) {
setState(() {
_ignoreFiltersOnSearch = val ?? false;
});
},
),
],
),
IconButton(
splashRadius: 20,
onPressed: () {
_showDraggableMenu(settings);
},
icon: Icon(
Icons.filter_list_sharp,
color: isNotFiltering ? null : Colors.yellow,
),
),
PopupMenuButton(
popUpAnimationStyle: popupAnimationStyle,
itemBuilder: (context) {
return [
PopupMenuItem<int>(
value: 0,
child: Text(context.l10n.update_library),
),
PopupMenuItem<int>(
value: 1,
child: Text(l10n.open_random_entry),
),
PopupMenuItem<int>(value: 2, child: Text(l10n.import)),
if (widget.itemType == ItemType.anime)
PopupMenuItem<int>(
value: 3,
child: Text(l10n.torrent_stream),
),
];
},
onSelected: (value) {
if (value == 0) {
manga.whenData((value) {
updateLibrary(
ref: ref,
context: context,
mangaList: value,
itemType: widget.itemType,
);
});
} else if (value == 1) {
manga.whenData((value) {
var randomManga = (value..shuffle()).first;
pushToMangaReaderDetail(
ref: ref,
archiveId: randomManga.isLocalArchive ?? false
? randomManga.id
: null,
context: context,
lang: randomManga.lang!,
mangaM: randomManga,
source: randomManga.source!,
sourceId: randomManga.sourceId,
);
});
} else if (value == 2) {
_importLocal(context, widget.itemType);
} else if (value == 3 &&
widget.itemType == ItemType.anime) {
addTorrent(context);
}
},
),
],
),
);
}
}
void _importLocal(BuildContext context, ItemType itemType) {
final l10n = l10nLocalizations(context)!;
final filesText = switch (itemType) {
ItemType.manga => ".zip, .cbz",
ItemType.anime => ".mp4, .mkv, .avi, and more",
ItemType.novel => ".epub",
};
bool isLoading = false;
showDialog(
context: context,
barrierDismissible: !isLoading,
builder: (context) {
return AlertDialog(
title: Text(l10n.import_local_file),
content: StatefulBuilder(
builder: (context, setState) {
return Consumer(
builder: (context, ref, child) {
return SizedBox(
height: 100,
child: Stack(
children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(3),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () async {
setState(() {
isLoading = true;
});
await ref.watch(
importArchivesFromFileProvider(
itemType: itemType,
null,
init: true,
).future,
);
setState(() {
isLoading = false;
});
Navigator.pop(context);
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.archive_outlined),
Text(
"${l10n.import_files} ( $filesText )",
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodySmall!.color,
fontSize: 10,
),
),
],
),
),
),
),
],
),
if (isLoading)
Container(
width: context.width(1),
height: context.height(1),
color: Colors.transparent,
child: UnconstrainedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(
context,
).scaffoldBackgroundColor,
),
height: 50,
width: 50,
child: const Center(child: ProgressCenter()),
),
),
),
],
),
);
},
);
},
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
],
),
],
);
},
);
}
void addTorrent(BuildContext context, {Manga? manga}) {
final l10n = l10nLocalizations(context)!;
String torrentUrl = "";
bool isLoading = false;
showDialog(
context: context,
barrierDismissible: !isLoading,
builder: (context) {
return AlertDialog(
title: Text(l10n.add_torrent),
content: StatefulBuilder(
builder: (context, setState) {
return Consumer(
builder: (context, ref, _) {
return SizedBox(
height: 150,
child: Column(
children: [
Row(
children: [
Expanded(
child: TextFormField(
onChanged: (value) {
setState(() {
torrentUrl = value;
});
},
decoration: InputDecoration(
hintText: l10n.enter_torrent_hint_text,
labelText: l10n.torrent_url,
isDense: true,
filled: true,
fillColor: Colors.transparent,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.secondaryColor,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.secondaryColor,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: context.secondaryColor,
),
),
),
),
),
TextButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
try {
await ref.watch(
addTorrentFromUrlOrFromFileProvider(
manga,
init: true,
url: torrentUrl,
).future,
);
} catch (_) {}
setState(() {
isLoading = false;
});
Navigator.pop(context);
},
child: Text(l10n.add),
),
],
),
Padding(
padding: const EdgeInsets.all(20),
child: Text(l10n.or),
),
Stack(
children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(3),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
try {
await ref.watch(
addTorrentFromUrlOrFromFileProvider(
manga,
init: true,
).future,
);
} catch (_) {}
setState(() {
isLoading = false;
});
Navigator.pop(context);
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.archive_outlined),
Text(
"import .torrent file",
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodySmall!.color,
fontSize: 10,
),
),
],
),
),
),
),
],
),
if (isLoading)
Positioned.fill(
child: Container(
width: 300,
height: 150,
color: Colors.transparent,
child: UnconstrainedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(
context,
).scaffoldBackgroundColor,
),
height: 50,
width: 50,
child: const Center(
child: ProgressCenter(),
),
),
),
),
),
],
),
],
),
);
},
);
},
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
],
),
],
);
},
);
}