From db209fea68277bf7d164e3a9a8452f9ce1b32cb3 Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Mon, 2 Jun 2025 23:36:04 +0200 Subject: [PATCH 1/8] + --- lib/services/trackers/myanimelist.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index d73334cb..23233c2f 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -149,6 +149,21 @@ class MyAnimeList extends _$MyAnimeList { ); } + Future> getGlobalData() async { + final accessToken = await _getAccessToken(); + final url = Uri.parse( + '$baseApiUrl/anime/ranking?ranking_type=airing&limit=15', + ); + final result = await _makeGetRequest(url, accessToken); + final res = jsonDecode(result.body) as Map; + + List mangaIds = res['data'] == null + ? [] + : (res['data'] as List).map((e) => e['node']["id"] as int).toList(); + + return []; + } + String _convertToIsoDate(int? epochTime) { String date = ""; try { From f1523391629f1f0b3d9b089befa1f6c2800dddd3 Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Tue, 3 Jun 2025 23:49:14 +0200 Subject: [PATCH 2/8] + --- .../tracker_library_screen.dart | 2286 +++++++++++++++++ lib/router/router.dart | 22 + lib/services/trackers/myanimelist.dart | 41 +- 3 files changed, 2342 insertions(+), 7 deletions(-) create mode 100644 lib/modules/tracker_library/tracker_library_screen.dart diff --git a/lib/modules/tracker_library/tracker_library_screen.dart b/lib/modules/tracker_library/tracker_library_screen.dart new file mode 100644 index 00000000..553bf9f0 --- /dev/null +++ b/lib/modules/tracker_library/tracker_library_screen.dart @@ -0,0 +1,2286 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; +import 'dart:math'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:isar/isar.dart'; +import 'package:mangayomi/eval/model/m_bridge.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/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/manga/detail/providers/update_manga_detail_providers.dart'; +import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart'; +import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.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/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/library/widgets/list_tile_manga_category.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/global_style.dart'; +import 'package:super_sliver_list/super_sliver_list.dart'; + +enum TrackerProviders { + anilist(syncId: 1), + myAnimeList(syncId: 2), + kitsu(syncId: 3); + + const TrackerProviders({ + required this.syncId, + }); + + final int syncId; +} + +class TrackerLibraryScreen extends ConsumerStatefulWidget { + final TrackerProviders trackerProvider; + final String? presetInput; + const TrackerLibraryScreen({ + required this.trackerProvider, + required this.presetInput, + super.key, + }); + + @override + ConsumerState createState() => _TrackerLibraryScreenState(); +} + +class _TrackerLibraryScreenState extends ConsumerState + with TickerProviderStateMixin { + bool _isSearch = false; + final List _entries = []; + final _textEditingController = TextEditingController(); + TabController? tabBarController; + int _tabIndex = 0; + + @override + void initState() { + super.initState(); + if (widget.presetInput != null) { + _isSearch = true; + _textEditingController.text = widget.presetInput!; + } + } + + Future _updateLibrary(List mangaList) async { + bool isDark = ref.read(themeModeStateProvider); + botToast( + context.l10n.updating_library("0", "0", "0"), + fontSize: 13, + second: 30, + alignY: !context.isTablet ? 0.85 : 1, + themeDark: isDark, + ); + int numbers = 0; + int failed = 0; + for (var manga in mangaList) { + try { + await ref.read( + updateMangaDetailProvider(mangaId: manga.id, isInit: false).future, + ); + } catch (_) { + failed++; + } + numbers++; + if (context.mounted) { + botToast( + context.l10n.updating_library(numbers, failed, mangaList.length), + fontSize: 13, + second: 10, + alignY: !context.isTablet ? 0.85 : 1, + animationDuration: 0, + dismissDirections: [DismissDirection.none], + onlyOne: false, + themeDark: isDark, + ); + } + } + await Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + if (mangaList.length == numbers) { + return false; + } + return true; + }); + BotToast.cleanAll(); + } + + @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 showCategoryTabs = ref.watch( + libraryShowCategoryTabsStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final mangaAll = ref.watch( + getAllMangaStreamProvider( + categoryId: null, + itemType: widget.itemType, + ), + ); + final l10n = l10nLocalizations(context)!; + return Scaffold( + body: mangaAll.when( + data: (man) { + return withoutCategories.when( + data: (withoutCategory) { + return categories.when( + data: (data) { + 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 (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) { + bool reverse = ref + .watch( + sortLibraryMangaStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ) + .reverse!; + + final continueReaderBtn = ref.watch( + libraryShowContinueReadingButtonStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final showNumbersOfItems = ref.watch( + libraryShowNumbersOfItemsStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final localSource = ref.watch( + libraryLocalSourceStateProvider( + 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 displayType = ref.watch( + libraryDisplayTypeStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final isNotFiltering = ref.watch( + mangasFilterResultStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final downloadFilterType = ref.watch( + mangaFilterDownloadedStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final unreadFilterType = ref.watch( + mangaFilterUnreadStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final startedFilterType = ref.watch( + mangaFilterStartedStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final bookmarkedFilterType = ref.watch( + mangaFilterBookmarkedStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final sortType = + ref + .watch( + sortLibraryMangaStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ) + .index + as int; + final numberOfItemsList = _filterAndSortManga( + data: man, + downloadFilterType: downloadFilterType, + unreadFilterType: unreadFilterType, + startedFilterType: startedFilterType, + bookmarkedFilterType: bookmarkedFilterType, + sortType: sortType, + ); + final withoutCategoryNumberOfItemsList = + _filterAndSortManga( + data: withoutCategory, + downloadFilterType: downloadFilterType, + unreadFilterType: unreadFilterType, + startedFilterType: startedFilterType, + bookmarkedFilterType: bookmarkedFilterType, + sortType: sortType, + ); + + 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, + ), + ], + ), + 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, + ), + ], + ), + ], + ), + Flexible( + child: TabBarView( + controller: tabBarController, + children: [ + if (withoutCategory.isNotEmpty) + for ( + var i = 0; + i < entr.length + 1; + i++ + ) + i == 0 + ? _bodyWithoutCategories( + withouCategories: 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, + ) + : _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, + ), + 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, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + return Consumer( + builder: (context, ref, child) { + bool reverse = + ref + .watch( + sortLibraryMangaStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ) + .reverse ?? + false; + final continueReaderBtn = ref.watch( + libraryShowContinueReadingButtonStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final showNumbersOfItems = ref.watch( + libraryShowNumbersOfItemsStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final localSource = ref.watch( + libraryLocalSourceStateProvider( + 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 displayType = ref.watch( + libraryDisplayTypeStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ); + final isNotFiltering = ref.watch( + mangasFilterResultStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final downloadFilterType = ref.watch( + mangaFilterDownloadedStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final unreadFilterType = ref.watch( + mangaFilterUnreadStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final startedFilterType = ref.watch( + mangaFilterStartedStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final bookmarkedFilterType = ref.watch( + mangaFilterBookmarkedStateProvider( + itemType: widget.itemType, + mangaList: _entries, + settings: settings, + ), + ); + final sortType = ref + .watch( + sortLibraryMangaStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ) + .index; + final numberOfItemsList = _filterAndSortManga( + data: man, + downloadFilterType: downloadFilterType, + unreadFilterType: unreadFilterType, + startedFilterType: startedFilterType, + bookmarkedFilterType: bookmarkedFilterType, + sortType: sortType ?? 0, + ); + 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, + ), + ); + }, + ); + }, + 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: Consumer( + builder: (context, ref, child) { + final isLongPressed = ref.watch(isLongPressedMangaStateProvider); + final color = Theme.of(context).textTheme.bodyLarge!.color!; + final mangaIds = ref.watch(mangasListStateProvider); + return AnimatedContainer( + curve: Curves.easeIn, + decoration: BoxDecoration( + color: context.primaryColor.withValues(alpha: 0.2), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + duration: const Duration(milliseconds: 100), + height: isLongPressed ? 70 : 0, + width: context.width(1), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: SizedBox( + height: 70, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shadowColor: Colors.transparent, + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + _openCategory(); + }, + child: Icon( + Icons.label_outline_rounded, + color: color, + ), + ), + ), + ), + Expanded( + child: SizedBox( + height: 70, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + ), + onPressed: () { + ref + .read( + mangasSetIsReadStateProvider( + mangaIds: mangaIds, + ).notifier, + ) + .set(); + ref.invalidate( + getAllMangaWithoutCategoriesStreamProvider( + itemType: widget.itemType, + ), + ); + ref.invalidate( + getAllMangaStreamProvider( + categoryId: null, + itemType: widget.itemType, + ), + ); + }, + child: Icon(Icons.done_all_sharp, color: color), + ), + ), + ), + Expanded( + child: SizedBox( + height: 70, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + ), + onPressed: () { + ref + .read( + mangasSetUnReadStateProvider( + mangaIds: mangaIds, + ).notifier, + ) + .set(); + ref.invalidate( + getAllMangaWithoutCategoriesStreamProvider( + itemType: widget.itemType, + ), + ); + ref.invalidate( + getAllMangaStreamProvider( + categoryId: null, + itemType: widget.itemType, + ), + ); + }, + child: Icon(Icons.remove_done_sharp, color: color), + ), + ), + ), + // Expanded( + // child: SizedBox( + // height: 70, + // child: ElevatedButton( + // style: ElevatedButton.styleFrom( + // elevation: 0, + // backgroundColor: Colors.transparent, + // shadowColor: Colors.transparent, + // ), + // onPressed: () {}, + // child: Icon( + // Icons.download_outlined, + // color: color, + // )), + // ), + // ), + Expanded( + child: SizedBox( + height: 70, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + ), + onPressed: () { + _deleteManga(); + }, + child: Icon( + Icons.delete_outline_outlined, + color: color, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ); + }, + 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, + }) { + 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!, + ); + 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, + }) { + 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!, + ); + if (entries.isNotEmpty) { + final entriesManga = reverse ? entries.reversed.toList() : entries; + return RefreshIndicator( + onRefresh: () async { + await _updateLibrary(data); + }, + 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 withouCategories = false, + required Settings settings, + }) { + final sortType = ref + .watch( + sortLibraryMangaStateProvider( + itemType: widget.itemType, + settings: settings, + ), + ) + .index; + final manga = withouCategories + ? 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, + ); + if (entries.isNotEmpty) { + final entriesManga = reverse ? entries.reversed.toList() : entries; + return RefreshIndicator( + onRefresh: () async { + await _updateLibrary(data); + }, + 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 _filterAndSortManga({ + required List data, + required int downloadFilterType, + required int unreadFilterType, + required int startedFilterType, + required int bookmarkedFilterType, + required int sortType, + }) { + List? 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) { + 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 _openCategory() { + List categoryIds = []; + showDialog( + context: context, + builder: (context) { + return Consumer( + builder: (context, ref, child) { + final mangaIdsList = ref.watch(mangasListStateProvider); + final l10n = l10nLocalizations(context)!; + final List mangasList = []; + for (var id in mangaIdsList) { + mangasList.add(isar.mangas.getSync(id)!); + } + return StatefulBuilder( + builder: (context, setState) { + return StreamBuilder( + stream: isar.categorys + .filter() + .idIsNotNull() + .and() + .forItemTypeEqualTo(widget.itemType) + .watch(fireImmediately: true), + builder: (context, snapshot) { + return AlertDialog( + title: Text(l10n.set_categories), + content: SizedBox( + width: context.width(0.8), + child: Builder( + builder: (context) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + final data = snapshot.data!; + data.sort( + (a, b) => (a.pos ?? 0).compareTo(b.pos ?? 0), + ); + + final entries = data + .where((e) => !(e.hide ?? false)) + .toList(); + if (entries.isEmpty) { + return Text(l10n.library_no_category_exist); + } + return SuperListView.builder( + shrinkWrap: true, + itemCount: entries.length, + itemBuilder: (context, index) { + return ListTileMangaCategory( + category: entries[index], + categoryIds: categoryIds, + mangasList: mangasList, + onTap: () { + setState(() { + if (categoryIds.contains( + entries[index].id, + )) { + categoryIds.remove(entries[index].id); + } else { + categoryIds.add(entries[index].id!); + } + }); + }, + res: (res) { + if (res.isNotEmpty) { + categoryIds.add(entries[index].id!); + } + }, + ); + }, + ); + } + return Text(l10n.library_no_category_exist); + }, + ), + ), + actions: [ + snapshot.hasData && snapshot.data!.isNotEmpty + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + context.push( + "/categories", + extra: ( + true, + widget.itemType == ItemType.manga + ? 0 + : widget.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: () { + isar.writeTxnSync(() { + for (var id in mangaIdsList) { + Manga? manga = isar.mangas + .getSync(id); + manga!.categories = categoryIds; + isar.mangas.putSync(manga); + ref + .read( + synchingProvider( + syncId: 1, + ).notifier, + ) + .addChangedPart( + ActionType.updateItem, + manga.id, + manga.toJson(), + false, + ); + } + }); + ref + .read( + mangasListStateProvider + .notifier, + ) + .clear(); + ref + .read( + isLongPressedMangaStateProvider + .notifier, + ) + .update(false); + + if (mounted) { + Navigator.pop(context); + } + }, + child: Text(l10n.ok), + ), + ], + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + context.push( + "/categories", + extra: ( + true, + widget.itemType == ItemType.manga + ? 0 + : widget.itemType == + ItemType.anime + ? 1 + : 2, + ), + ); + Navigator.pop(context); + }, + child: Text(l10n.edit_categories), + ), + ], + ), + ], + ); + }, + ); + }, + ); + }, + ); + }, + ); + } + + void _deleteManga() { + List fromLibList = []; + List downloadedChapsList = []; + showDialog( + context: context, + builder: (context) { + return Consumer( + builder: (context, ref, child) { + final mangaIdsList = ref.watch(mangasListStateProvider); + final l10n = l10nLocalizations(context)!; + final List 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 { + if (fromLibList.isNotEmpty) { + isar.writeTxnSync(() { + for (var manga in mangasList) { + if (manga.isLocalArchive ?? false) { + final histories = isar.historys + .filter() + .mangaIdEqualTo(manga.id) + .findAllSync(); + for (var history in histories) { + isar.historys.deleteSync(history.id!); + } + + for (var chapter in manga.chapters) { + isar.updates + .filter() + .mangaIdEqualTo(chapter.mangaId) + .chapterNameEqualTo(chapter.name) + .deleteAllSync(); + isar.chapters.deleteSync(chapter.id!); + } + isar.mangas.deleteSync(manga.id!); + ref + .read( + synchingProvider(syncId: 1).notifier, + ) + .addChangedPart( + ActionType.removeItem, + manga.id, + "{}", + false, + ); + } else { + manga.favorite = false; + isar.mangas.putSync(manga); + ref + .read( + synchingProvider(syncId: 1).notifier, + ) + .addChangedPart( + ActionType.updateItem, + manga.id, + manga.toJson(), + false, + ); + } + } + }); + } + if (downloadedChapsList.isNotEmpty) { + isar.writeTxnSync(() async { + for (var manga in mangasList) { + if (manga.isLocalArchive ?? false) { + for (var chapter in manga.chapters) { + try { + final storageProvider = + StorageProvider(); + final mangaDir = await storageProvider + .getMangaMainDirectory(chapter); + final path = await storageProvider + .getMangaChapterDirectory( + chapter, + mangaMainDirectory: mangaDir, + ); + + try { + try { + if (File( + "${mangaDir!.path}${chapter.name}.cbz", + ).existsSync()) { + File( + "${mangaDir.path}${chapter.name}.cbz", + ).deleteSync(); + } + } catch (_) {} + try { + if (File( + "${mangaDir!.path}${chapter.name}.mp4", + ).existsSync()) { + File( + "${mangaDir.path}${chapter.name}.mp4", + ).deleteSync(); + } + } catch (_) {} + path!.deleteSync(recursive: true); + } catch (_) {} + isar.writeTxnSync(() { + final download = isar.downloads + .filter() + .idEqualTo(chapter.id!) + .findAllSync(); + if (download.isNotEmpty) { + isar.downloads.deleteSync( + download.first.id!, + ); + } + }); + } catch (_) {} + } + } + } + }); + } + + ref.read(mangasListStateProvider.notifier).clear(); + ref + .read(isLongPressedMangaStateProvider.notifier) + .update(false); + if (mounted) { + Navigator.pop(context); + } + }, + child: Text(l10n.ok), + ), + ], + ), + ], + ); + }, + ); + }, + ); + }, + ); + } + + 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 mangas, + bool isCategory, + int? categoryId, + Settings settings, + ) { + final isLongPressed = ref.watch(isLongPressedMangaStateProvider); + 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(isLongPressedMangaStateProvider.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(isLongPressedMangaStateProvider.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 == ItemType.manga + ? l10n.manga + : widget.itemType == ItemType.anime + ? l10n.anime + : l10n.novel, + 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 aren’t 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( + value: 0, + child: Text(context.l10n.update_library), + ), + PopupMenuItem( + value: 1, + child: Text(l10n.open_random_entry), + ), + ]; + }, + onSelected: (value) { + if (value == 0) { + manga.whenData((value) { + _updateLibrary(value); + }); + } 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!, + ); + }); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/router/router.dart b/lib/router/router.dart index 09bf69e3..d324f246 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -18,6 +18,7 @@ import 'package:mangayomi/modules/more/settings/browse/source_repositories.dart' import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/more/statistics/statistics_screen.dart'; import 'package:mangayomi/modules/novel/novel_reader_view.dart'; +import 'package:mangayomi/modules/tracker_library/tracker_library_screen.dart'; import 'package:mangayomi/modules/updates/updates_screen.dart'; import 'package:mangayomi/modules/more/categories/categories_screen.dart'; import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart'; @@ -124,6 +125,27 @@ class RouterNotifier extends ChangeNotifier { ), _genericRoute(name: "history", child: const HistoryScreen()), _genericRoute(name: "updates", child: const UpdatesScreen()), + _genericRoute( + name: "trackerLibrary/anilist", + builder: (id) => TrackerLibraryScreen( + trackerProvider: TrackerProviders.anilist, + presetInput: id, + ), + ), + _genericRoute( + name: "trackerLibrary/kitsu", + builder: (id) => TrackerLibraryScreen( + trackerProvider: TrackerProviders.kitsu, + presetInput: id, + ), + ), + _genericRoute( + name: "trackerLibrary/mal", + builder: (id) => TrackerLibraryScreen( + trackerProvider: TrackerProviders.myAnimeList, + presetInput: id, + ), + ), _genericRoute(name: "browse", child: const BrowseScreen()), _genericRoute(name: "more", child: const MoreScreen()), ], diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index 23233c2f..7ad67f6b 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -149,19 +149,46 @@ class MyAnimeList extends _$MyAnimeList { ); } - Future> getGlobalData() async { + Future> getGlobalData(bool isManga) async { final accessToken = await _getAccessToken(); - final url = Uri.parse( - '$baseApiUrl/anime/ranking?ranking_type=airing&limit=15', + final item = isManga ? "manga" : "anime"; + final contentUnit = isManga ? "num_chapters" : "num_episodes"; + final url = Uri.parse('$baseApiUrl/$item/ranking').replace( + queryParameters: { + 'ranking_type': 'airing', + 'limit': '15', + 'fields': + 'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date,mean', + }, ); final result = await _makeGetRequest(url, accessToken); final res = jsonDecode(result.body) as Map; - List mangaIds = res['data'] == null + return res['data'] == null ? [] - : (res['data'] as List).map((e) => e['node']["id"] as int).toList(); - - return []; + : (res['data'] as List) + .map( + (e) => TrackSearch( + mediaId: e["node"]["id"], + summary: e["node"]["synopsis"] ?? "", + totalChapter: e["node"][contentUnit], + coverUrl: e["node"]["main_picture"]["large"] ?? "", + title: e["node"]["title"], + score: e["node"]["mean"], + startDate: e["node"]["start_date"] ?? "", + publishingType: e["node"]["media_type"].toString().replaceAll( + "_", + " ", + ), + publishingStatus: e["node"]["status"].toString().replaceAll( + "_", + " ", + ), + trackingUrl: + "https://myanimelist.net/$item/${e["node"]["id"]}", + ), + ) + .toList(); } String _convertToIsoDate(int? epochTime) { From eeed17c264945b80765eb39bb92029e6ba04fe6f Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Wed, 4 Jun 2025 15:39:14 +0200 Subject: [PATCH 3/8] adding Trakt --- lib/services/trackers/trakt.dart | 314 +++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 lib/services/trackers/trakt.dart diff --git a/lib/services/trackers/trakt.dart b/lib/services/trackers/trakt.dart new file mode 100644 index 00000000..cec3db72 --- /dev/null +++ b/lib/services/trackers/trakt.dart @@ -0,0 +1,314 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:intl/intl.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/models/track.dart'; +import 'package:mangayomi/models/track_preference.dart'; +import 'package:mangayomi/models/track_search.dart'; +import 'package:mangayomi/modules/more/settings/track/myanimelist/model.dart'; +import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart'; +import 'package:mangayomi/services/http/m_client.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'trakt.g.dart'; + +@riverpod +class Trakt extends _$Trakt { + final http = MClient.init(reqcopyWith: {'useDartHttpClient': true}); + String baseOAuthUrl = 'https://api.trakt.tv/oauth/authorize?response_type=code&client_id=%20&redirect_uri=%20&state=%20'; + String baseApiUrl = 'https://api.trakt.tv'; + String codeVerifier = ""; + static final isDesktop = (Platform.isWindows || Platform.isLinux); + String clientId = isDesktop + ? '5520c7e24da0d8d73ec80315b61b9849483583b013cb7f296c6db723eb9886a1' + : '5520c7e24da0d8d73ec80315b61b9849483583b013cb7f296c6db723eb9886a1'; + + @override + void build({required int syncId, required ItemType? itemType}) {} + + Future login() async { + final callbackUrlScheme = isDesktop + ? 'http://localhost:43824' + : 'mangayomi'; + final loginUrl = _authUrl(); + + try { + final uri = await FlutterWebAuth2.authenticate( + url: loginUrl, + callbackUrlScheme: callbackUrlScheme, + ); + final queryParams = Uri.parse(uri).queryParameters; + if (queryParams['code'] == null) return null; + + final oAuth = await _getOAuth(queryParams['code']!); + final mALOAuth = OAuth.fromJson(oAuth as Map) + ..expiresIn = DateTime.now() + .add(Duration(seconds: oAuth['expires_in'])) + .millisecondsSinceEpoch; + final username = await _getUserName(mALOAuth.accessToken!); + ref + .read(tracksProvider(syncId: syncId).notifier) + .login( + TrackPreference( + syncId: syncId, + username: username, + oAuth: jsonEncode(mALOAuth.toJson()), + ), + ); + + return true; + } catch (_) { + return false; + } + } + + Future _getAccessToken() async { + final track = ref.watch(tracksProvider(syncId: syncId)); + final mALOAuth = OAuth.fromJson( + jsonDecode(track!.oAuth!) as Map, + ); + final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!); + if (DateTime.now().isAfter(expiresIn)) { + final params = { + 'client_id': clientId, + 'grant_type': 'refresh_token', + 'refresh_token': mALOAuth.refreshToken, + }; + final response = await http.post( + Uri.parse('$baseOAuthUrl/token'), + body: params, + ); + final oAuth = OAuth.fromJson( + jsonDecode(response.body) as Map, + ); + final username = await _getUserName(oAuth.accessToken!); + ref + .read(tracksProvider(syncId: syncId).notifier) + .login( + TrackPreference( + syncId: syncId, + username: username, + prefs: "", + oAuth: jsonEncode(oAuth.toJson()), + ), + ); + return oAuth.accessToken!; + } + return mALOAuth.accessToken!; + } + + Future> search(String query, isManga) async { + final accessToken = await _getAccessToken(); + final url = Uri.parse( + '$baseApiUrl/${isManga ? "manga" : "anime"}', + ).replace(queryParameters: {'q': query.trim(), 'nsfw': 'true'}); + final result = await _makeGetRequest(url, accessToken); + final res = jsonDecode(result.body) as Map; + + List mangaIds = res['data'] == null + ? [] + : (res['data'] as List).map((e) => e['node']["id"] as int).toList(); + final trackSearchResult = await Future.wait( + mangaIds.map((id) => getDetails(id, accessToken, isManga)), + ); + + return trackSearchResult + .where((element) => !element.publishingType!.contains("novel")) + .toList(); + } + + Future getDetails( + int id, + String accessToken, + bool isManga, + ) async { + final item = isManga ? "manga" : "anime"; + final contentUnit = isManga ? "num_chapters" : "num_episodes"; + final url = Uri.parse('$baseApiUrl/$item/$id').replace( + queryParameters: { + 'fields': + 'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date', + }, + ); + + final result = await _makeGetRequest(url, accessToken); + final res = jsonDecode(result.body) as Map; + + return TrackSearch( + mediaId: res["id"], + summary: res["synopsis"] ?? "", + totalChapter: res[contentUnit], + coverUrl: res["main_picture"]["large"] ?? "", + title: res["title"], + startDate: res["start_date"] ?? "", + publishingType: res["media_type"].toString().replaceAll("_", " "), + publishingStatus: res["status"].toString().replaceAll("_", " "), + trackingUrl: "https://myanimelist.net/$item/${res["id"]}", + ); + } + + String _convertToIsoDate(int? epochTime) { + String date = ""; + try { + date = DateFormat( + "yyyy-MM-dd", + "en_US", + ).format(DateTime.fromMillisecondsSinceEpoch(epochTime!)); + } catch (_) {} + return date; + } + + String _codeVerifier() { + final random = Random.secure(); + final values = List.generate(200, (i) => random.nextInt(256)); + codeVerifier = base64UrlEncode(values).substring(0, 128); + return codeVerifier; + } + + String _authUrl() { + _codeVerifier(); + return '$baseOAuthUrl/authorize?client_id=$clientId&code_challenge=$codeVerifier&response_type=code'; + } + + TrackStatus _getMALTrackStatus(String status, bool isManga) { + return switch (status) { + "reading" when isManga => TrackStatus.reading, + "watching" when !isManga => TrackStatus.watching, + "completed" => TrackStatus.completed, + "on_hold" => TrackStatus.onHold, + "dropped" => TrackStatus.dropped, + "plan_to_read" when isManga => TrackStatus.planToRead, + "plan_to_watch" when !isManga => TrackStatus.planToWatch, + _ => isManga ? TrackStatus.reReading : TrackStatus.planToWatch, + }; + } + + List statusList(bool isManga) => [ + isManga ? TrackStatus.reading : TrackStatus.watching, + TrackStatus.completed, + TrackStatus.onHold, + TrackStatus.dropped, + isManga ? TrackStatus.planToRead : TrackStatus.planToWatch, + if (isManga) TrackStatus.reReading, + ]; + + String? toMyAnimeListStatus(TrackStatus status, bool isManga) { + return switch (status) { + TrackStatus.reading when isManga => "reading", + TrackStatus.watching when !isManga => "watching", + TrackStatus.completed => "completed", + TrackStatus.onHold => "on_hold", + TrackStatus.dropped => "dropped", + TrackStatus.planToRead when isManga => "plan_to_read", + TrackStatus.planToWatch when !isManga => "plan_to_watch", + _ => isManga ? "reading" : "plan_to_watch", + }; + } + + Future _getOAuth(String code) async { + final params = { + 'client_id': clientId, + 'code': code, + 'code_verifier': codeVerifier, + 'grant_type': 'authorization_code', + }; + final response = await http.post( + Uri.parse('$baseOAuthUrl/token'), + body: params, + ); + return jsonDecode(response.body); + } + + Future _getUserName(String accessToken) async { + final response = await _makeGetRequest( + Uri.parse('$baseApiUrl/users/@me'), + accessToken, + ); + return jsonDecode(response.body)['name']; + } + + Future findLibItem(Track track, bool isManga) async { + final type = isManga ? "manga" : "anime"; + final contentUnit = isManga ? 'num_chapters' : 'num_episodes'; + final accessToken = await _getAccessToken(); + final uri = Uri.parse('$baseApiUrl/$type/${track.mediaId}').replace( + queryParameters: { + 'fields': '$contentUnit,my_list_status{start_date,finish_date}', + }, + ); + final response = await _makeGetRequest(uri, accessToken); + final mJson = jsonDecode(response.body); + track.totalChapter = mJson[contentUnit] ?? 0; + if (mJson['my_list_status'] != null) { + track = _parseItem(mJson["my_list_status"], track, isManga); + } else { + track = await update(track, isManga); + } + return track; + } + + Track _parseItem(Map mJson, Track track, bool isManga) { + bool isRepeating = + mJson[isManga ? "is_rereading" : "is_rewatching"] ?? false; + track.status = isRepeating + ? (isManga ? TrackStatus.reReading : TrackStatus.reWatching) + : _getMALTrackStatus(mJson["status"], isManga); + track.lastChapterRead = int.parse( + mJson[isManga ? "num_chapters_read" : "num_episodes_watched"].toString(), + ); + track.score = int.parse(mJson["score"].toString()); + track.startedReadingDate = _parseDate(mJson["start_date"]); + track.finishedReadingDate = _parseDate(mJson["finish_date"]); + return track; + } + + int? _parseDate(String? isoDate) { + if (isoDate == null) return null; + + final date = DateFormat('yyyy-MM-dd', 'en_US').parse(isoDate); + return date.millisecondsSinceEpoch; + } + + Future update(Track track, bool isManga) async { + final accessToken = await _getAccessToken(); + final formBody = { + 'status': + (toMyAnimeListStatus(track.status, isManga) ?? + (isManga ? 'reading' : 'watching')) + .toString(), + isManga ? 'is_rereading' : 'is_rewatching': + (track.status == + (isManga ? TrackStatus.reReading : TrackStatus.reWatching)) + .toString(), + 'score': track.score.toString(), + isManga ? 'num_chapters_read' : 'num_watched_episodes': track + .lastChapterRead + .toString(), + if (track.startedReadingDate != null) + 'start_date': _convertToIsoDate(track.startedReadingDate), + if (track.finishedReadingDate != null) + 'finish_date': _convertToIsoDate(track.finishedReadingDate), + }; + final request = Request( + 'PUT', + Uri.parse( + '$baseApiUrl/${isManga ? "manga" : "anime"}' + '/${track.mediaId}/my_list_status', + ), + ); + request.bodyFields = formBody; + request.headers.addAll({'Authorization': 'Bearer $accessToken'}); + final response = await Client().send(request); + final mJson = jsonDecode(await response.stream.bytesToString()); + return _parseItem(mJson, track, isManga); + } + + Future _makeGetRequest(Uri url, String accessToken) async { + return await http.get( + url, + headers: {'Authorization': 'Bearer $accessToken'}, + ); + } +} From 1b4177b59b358e1cea7edf78959722beb7baeb28 Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Thu, 5 Jun 2025 00:19:47 +0200 Subject: [PATCH 4/8] + --- .../providers/track_state_providers.dart | 6 + .../tracker_library_screen.dart | 2618 +++-------------- lib/services/trackers/myanimelist.dart | 7 +- 3 files changed, 460 insertions(+), 2171 deletions(-) diff --git a/lib/modules/manga/detail/providers/track_state_providers.dart b/lib/modules/manga/detail/providers/track_state_providers.dart index eb25567e..833550b2 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.dart @@ -138,4 +138,10 @@ class TrackState extends _$TrackState { final tracker = getNotifier(syncId); return await tracker.search(query, _isManga); } + + Future?> fetchData({String rankingType = "airing"}) async { + final syncId = track!.syncId!; + final tracker = getNotifier(syncId); + return await tracker.fetchData(_isManga, rankingType); + } } diff --git a/lib/modules/tracker_library/tracker_library_screen.dart b/lib/modules/tracker_library/tracker_library_screen.dart index 553bf9f0..317fd783 100644 --- a/lib/modules/tracker_library/tracker_library_screen.dart +++ b/lib/modules/tracker_library/tracker_library_screen.dart @@ -43,7 +43,8 @@ import 'package:super_sliver_list/super_sliver_list.dart'; enum TrackerProviders { anilist(syncId: 1), myAnimeList(syncId: 2), - kitsu(syncId: 3); + kitsu(syncId: 3), + trakt(syncId: 4); const TrackerProviders({ required this.syncId, @@ -54,1492 +55,486 @@ enum TrackerProviders { class TrackerLibraryScreen extends ConsumerStatefulWidget { final TrackerProviders trackerProvider; - final String? presetInput; - const TrackerLibraryScreen({ - required this.trackerProvider, - required this.presetInput, - super.key, - }); + const TrackerLibraryScreen({required this.trackerProvider, super.key}); @override ConsumerState createState() => _TrackerLibraryScreenState(); } -class _TrackerLibraryScreenState extends ConsumerState - with TickerProviderStateMixin { - bool _isSearch = false; - final List _entries = []; - final _textEditingController = TextEditingController(); - TabController? tabBarController; - int _tabIndex = 0; +class _TrackerLibraryScreenState extends ConsumerState { + @override + Widget build(BuildContext context) { + final l10n = l10nLocalizations(context)!; + return Scaffold( + appBar: AppBar(title: Text(l10n.migrate)), + body: 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( + query: widget.manga.name ?? widget.manga.author ?? "", + manga: widget.manga, + source: source, + ), + ); + }, + ) + : Container(), + ); + } +} + +class MigrationSourceSearchScreen extends StatefulWidget { + final String query; + final Manga manga; + + final Source source; + const MigrationSourceSearchScreen({ + super.key, + required this.query, + required this.manga, + required this.source, + }); + + @override + State createState() => + _MigrationSourceSearchScreenState(); +} + +class _MigrationSourceSearchScreenState + extends State { @override void initState() { super.initState(); - if (widget.presetInput != null) { - _isSearch = true; - _textEditingController.text = widget.presetInput!; - } + _init(); } - Future _updateLibrary(List mangaList) async { - bool isDark = ref.read(themeModeStateProvider); - botToast( - context.l10n.updating_library("0", "0", "0"), - fontSize: 13, - second: 30, - alignY: !context.isTablet ? 0.85 : 1, - themeDark: isDark, - ); - int numbers = 0; - int failed = 0; - for (var manga in mangaList) { - try { - await ref.read( - updateMangaDetailProvider(mangaId: manga.id, isInit: false).future, - ); - } catch (_) { - failed++; + String _errorMessage = ""; + bool _isLoading = true; + MPages? pages; + _init() async { + try { + _errorMessage = ""; + pages = await search( + source: widget.source, + page: 1, + query: widget.query, + filterList: [], + ); + if (mounted) { + setState(() { + _isLoading = false; + }); } - numbers++; - if (context.mounted) { - botToast( - context.l10n.updating_library(numbers, failed, mangaList.length), - fontSize: 13, - second: 10, - alignY: !context.isTablet ? 0.85 : 1, - animationDuration: 0, - dismissDirections: [DismissDirection.none], - onlyOne: false, - themeDark: isDark, - ); + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = e.toString(); + _isLoading = false; + }); } } - await Future.doWhile(() async { - await Future.delayed(const Duration(seconds: 1)); - if (mangaList.length == numbers) { - return false; - } - return true; - }); - BotToast.cleanAll(); } @override Widget build(BuildContext context) { - final settingsStream = ref.watch(getSettingsStreamProvider); - return settingsStream.when( - data: (settingsList) { - final settings = settingsList.first; + final l10n = l10nLocalizations(context)!; - final categories = ref.watch( - getMangaCategorieStreamProvider(itemType: widget.itemType), - ); - final withoutCategories = ref.watch( - getAllMangaWithoutCategoriesStreamProvider(itemType: widget.itemType), - ); - final showCategoryTabs = ref.watch( - libraryShowCategoryTabsStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final mangaAll = ref.watch( - getAllMangaStreamProvider( - categoryId: null, - itemType: widget.itemType, - ), - ); - final l10n = l10nLocalizations(context)!; - return Scaffold( - body: mangaAll.when( - data: (man) { - return withoutCategories.when( - data: (withoutCategory) { - return categories.when( - data: (data) { - 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 (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 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, + ); + }, + ); + } + return Center(child: Text(l10n.no_result)); + }, + ), + ), + ], + ), + ), + ); + } +} - return Consumer( - builder: (context, ref, child) { - bool reverse = ref - .watch( - sortLibraryMangaStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ) - .reverse!; +class MigrationMangaGlobalImageCard extends ConsumerStatefulWidget { + final Manga oldManga; + final MManga manga; + final Source source; - final continueReaderBtn = ref.watch( - libraryShowContinueReadingButtonStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final showNumbersOfItems = ref.watch( - libraryShowNumbersOfItemsStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final localSource = ref.watch( - libraryLocalSourceStateProvider( - 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 displayType = ref.watch( - libraryDisplayTypeStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final isNotFiltering = ref.watch( - mangasFilterResultStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final downloadFilterType = ref.watch( - mangaFilterDownloadedStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final unreadFilterType = ref.watch( - mangaFilterUnreadStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final startedFilterType = ref.watch( - mangaFilterStartedStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final bookmarkedFilterType = ref.watch( - mangaFilterBookmarkedStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final sortType = - ref - .watch( - sortLibraryMangaStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ) - .index - as int; - final numberOfItemsList = _filterAndSortManga( - data: man, - downloadFilterType: downloadFilterType, - unreadFilterType: unreadFilterType, - startedFilterType: startedFilterType, - bookmarkedFilterType: bookmarkedFilterType, - sortType: sortType, - ); - final withoutCategoryNumberOfItemsList = - _filterAndSortManga( - data: withoutCategory, - downloadFilterType: downloadFilterType, - unreadFilterType: unreadFilterType, - startedFilterType: startedFilterType, - bookmarkedFilterType: bookmarkedFilterType, - sortType: sortType, - ); + const MigrationMangaGlobalImageCard({ + super.key, + required this.oldManga, + required this.manga, + required this.source, + }); - 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, - ), - ], - ), - 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, - ), - ], - ), - ], - ), - Flexible( - child: TabBarView( - controller: tabBarController, - children: [ - if (withoutCategory.isNotEmpty) - for ( - var i = 0; - i < entr.length + 1; - i++ - ) - i == 0 - ? _bodyWithoutCategories( - withouCategories: 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, - ) - : _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, - ), - 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, - ), - ], - ), - ), - ], + @override + ConsumerState createState() => + _MigrationMangaGlobalImageCardState(); +} + +class _MigrationMangaGlobalImageCardState + extends ConsumerState + with AutomaticKeepAliveClientMixin { + @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!, ), ), - ); - }, - ); - } - return Consumer( - builder: (context, ref, child) { - bool reverse = - ref - .watch( - sortLibraryMangaStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ) - .reverse ?? - false; - final continueReaderBtn = ref.watch( - libraryShowContinueReadingButtonStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final showNumbersOfItems = ref.watch( - libraryShowNumbersOfItemsStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final localSource = ref.watch( - libraryLocalSourceStateProvider( - 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 displayType = ref.watch( - libraryDisplayTypeStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ); - final isNotFiltering = ref.watch( - mangasFilterResultStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final downloadFilterType = ref.watch( - mangaFilterDownloadedStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final unreadFilterType = ref.watch( - mangaFilterUnreadStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final startedFilterType = ref.watch( - mangaFilterStartedStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final bookmarkedFilterType = ref.watch( - mangaFilterBookmarkedStateProvider( - itemType: widget.itemType, - mangaList: _entries, - settings: settings, - ), - ); - final sortType = ref - .watch( - sortLibraryMangaStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ) - .index; - final numberOfItemsList = _filterAndSortManga( - data: man, - downloadFilterType: downloadFilterType, - unreadFilterType: unreadFilterType, - startedFilterType: startedFilterType, - bookmarkedFilterType: bookmarkedFilterType, - sortType: sortType ?? 0, - ); - 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, + imageUrl: toImgUrl( + hasData + ? snapshot + .data! + .first + .customCoverFromTracker ?? + snapshot.data!.first.imageUrl ?? + "" + : getMangaDetail.imageUrl ?? "", + ), + width: 110, + height: 150, + fit: BoxFit.cover, ), ); }, - ); - }, - 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: Consumer( - builder: (context, ref, child) { - final isLongPressed = ref.watch(isLongPressedMangaStateProvider); - final color = Theme.of(context).textTheme.bodyLarge!.color!; - final mangaIds = ref.watch(mangasListStateProvider); - return AnimatedContainer( - curve: Curves.easeIn, - decoration: BoxDecoration( - color: context.primaryColor.withValues(alpha: 0.2), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), + ), + BottomTextWidget( + fontSize: 12.0, + text: widget.manga.name!, + isLoading: true, + textColor: Theme.of(context).textTheme.bodyLarge!.color, + isComfortableGrid: true, + ), + ], ), ), - duration: const Duration(milliseconds: 100), - height: isLongPressed ? 70 : 0, - width: context.width(1), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: SizedBox( - height: 70, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shadowColor: Colors.transparent, - elevation: 0, - backgroundColor: Colors.transparent, - ), - onPressed: () { - _openCategory(); - }, - child: Icon( - Icons.label_outline_rounded, - color: color, - ), - ), - ), - ), - Expanded( - child: SizedBox( - height: 70, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - ), - onPressed: () { - ref - .read( - mangasSetIsReadStateProvider( - mangaIds: mangaIds, - ).notifier, - ) - .set(); - ref.invalidate( - getAllMangaWithoutCategoriesStreamProvider( - itemType: widget.itemType, - ), - ); - ref.invalidate( - getAllMangaStreamProvider( - categoryId: null, - itemType: widget.itemType, - ), - ); - }, - child: Icon(Icons.done_all_sharp, color: color), - ), - ), - ), - Expanded( - child: SizedBox( - height: 70, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - ), - onPressed: () { - ref - .read( - mangasSetUnReadStateProvider( - mangaIds: mangaIds, - ).notifier, - ) - .set(); - ref.invalidate( - getAllMangaWithoutCategoriesStreamProvider( - itemType: widget.itemType, - ), - ); - ref.invalidate( - getAllMangaStreamProvider( - categoryId: null, - itemType: widget.itemType, - ), - ); - }, - child: Icon(Icons.remove_done_sharp, color: color), - ), - ), - ), - // Expanded( - // child: SizedBox( - // height: 70, - // child: ElevatedButton( - // style: ElevatedButton.styleFrom( - // elevation: 0, - // backgroundColor: Colors.transparent, - // shadowColor: Colors.transparent, - // ), - // onPressed: () {}, - // child: Icon( - // Icons.download_outlined, - // color: color, - // )), - // ), - // ), - Expanded( - child: SizedBox( - height: 70, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - ), - onPressed: () { - _deleteManga(); - }, - child: Icon( - Icons.delete_outline_outlined, - color: color, - ), - ), - ), - ), - ], + Container( + width: 110, + height: 150, + color: hasData && snapshot.data!.first.favorite! + ? Colors.black.withValues(alpha: 0.7) + : null, ), - ); - }, - ), - ); - }, - 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, - }) { - 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!, - ); - 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, - }) { - 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!, - ); - if (entries.isNotEmpty) { - final entriesManga = reverse ? entries.reversed.toList() : entries; - return RefreshIndicator( - onRefresh: () async { - await _updateLibrary(data); - }, - 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 withouCategories = false, - required Settings settings, - }) { - final sortType = ref - .watch( - sortLibraryMangaStateProvider( - itemType: widget.itemType, - settings: settings, - ), - ) - .index; - final manga = withouCategories - ? 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, - ); - if (entries.isNotEmpty) { - final entriesManga = reverse ? entries.reversed.toList() : entries; - return RefreshIndicator( - onRefresh: () async { - await _updateLibrary(data); - }, - 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 _filterAndSortManga({ - required List data, - required int downloadFilterType, - required int unreadFilterType, - required int startedFilterType, - required int bookmarkedFilterType, - required int sortType, - }) { - List? 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) { - 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 _openCategory() { - List categoryIds = []; - showDialog( - context: context, - builder: (context) { - return Consumer( - builder: (context, ref, child) { - final mangaIdsList = ref.watch(mangasListStateProvider); - final l10n = l10nLocalizations(context)!; - final List mangasList = []; - for (var id in mangaIdsList) { - mangasList.add(isar.mangas.getSync(id)!); - } - return StatefulBuilder( - builder: (context, setState) { - return StreamBuilder( - stream: isar.categorys - .filter() - .idIsNotNull() - .and() - .forItemTypeEqualTo(widget.itemType) - .watch(fireImmediately: true), - builder: (context, snapshot) { - return AlertDialog( - title: Text(l10n.set_categories), - content: SizedBox( - width: context.width(0.8), - child: Builder( - builder: (context) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - final data = snapshot.data!; - data.sort( - (a, b) => (a.pos ?? 0).compareTo(b.pos ?? 0), - ); - - final entries = data - .where((e) => !(e.hide ?? false)) - .toList(); - if (entries.isEmpty) { - return Text(l10n.library_no_category_exist); - } - return SuperListView.builder( - shrinkWrap: true, - itemCount: entries.length, - itemBuilder: (context, index) { - return ListTileMangaCategory( - category: entries[index], - categoryIds: categoryIds, - mangasList: mangasList, - onTap: () { - setState(() { - if (categoryIds.contains( - entries[index].id, - )) { - categoryIds.remove(entries[index].id); - } else { - categoryIds.add(entries[index].id!); - } - }); - }, - res: (res) { - if (res.isNotEmpty) { - categoryIds.add(entries[index].id!); - } - }, - ); - }, - ); - } - return Text(l10n.library_no_category_exist); - }, - ), + 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, ), - actions: [ - snapshot.hasData && snapshot.data!.isNotEmpty - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: () { - context.push( - "/categories", - extra: ( - true, - widget.itemType == ItemType.manga - ? 0 - : widget.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: () { - isar.writeTxnSync(() { - for (var id in mangaIdsList) { - Manga? manga = isar.mangas - .getSync(id); - manga!.categories = categoryIds; - isar.mangas.putSync(manga); - ref - .read( - synchingProvider( - syncId: 1, - ).notifier, - ) - .addChangedPart( - ActionType.updateItem, - manga.id, - manga.toJson(), - false, - ); - } - }); - ref - .read( - mangasListStateProvider - .notifier, - ) - .clear(); - ref - .read( - isLongPressedMangaStateProvider - .notifier, - ) - .update(false); - - if (mounted) { - Navigator.pop(context); - } - }, - child: Text(l10n.ok), - ), - ], - ), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - context.push( - "/categories", - extra: ( - true, - widget.itemType == ItemType.manga - ? 0 - : widget.itemType == - ItemType.anime - ? 1 - : 2, - ), - ); - Navigator.pop(context); - }, - child: Text(l10n.edit_categories), - ), - ], - ), - ], - ); - }, - ); - }, - ); - }, - ); - }, + ), + ), + ], + ), + ); + }, + ), ); } - void _deleteManga() { - List fromLibList = []; - List downloadedChapsList = []; - showDialog( - context: context, - builder: (context) { - return Consumer( - builder: (context, ref, child) { - final mangaIdsList = ref.watch(mangasListStateProvider); - final l10n = l10nLocalizations(context)!; - final List mangasList = []; - for (var id in mangaIdsList) { - mangasList.add(isar.mangas.getSync(id)!); - } - return StatefulBuilder( - builder: (context, setState) { + @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(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, - ), - ], - ), - ), + title: Text(l10n.migrate_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(context); + Navigator.pop(ctx); }, child: Text(l10n.cancel), ), const SizedBox(width: 15), - TextButton( - onPressed: () async { - if (fromLibList.isNotEmpty) { + Consumer( + builder: (context, ref, child) => TextButton( + onPressed: () async { + String? historyChapter; + String? historyDate; + List chaptersProgress = []; isar.writeTxnSync(() { - for (var manga in mangasList) { - if (manga.isLocalArchive ?? false) { - final histories = isar.historys - .filter() - .mangaIdEqualTo(manga.id) - .findAllSync(); - for (var history in histories) { - isar.historys.deleteSync(history.id!); - } - - for (var chapter in manga.chapters) { - isar.updates - .filter() - .mangaIdEqualTo(chapter.mangaId) - .chapterNameEqualTo(chapter.name) - .deleteAllSync(); - isar.chapters.deleteSync(chapter.id!); - } - isar.mangas.deleteSync(manga.id!); - ref - .read( - synchingProvider(syncId: 1).notifier, - ) - .addChangedPart( - ActionType.removeItem, - manga.id, - "{}", - false, - ); - } else { - manga.favorite = false; - isar.mangas.putSync(manga); - ref - .read( - synchingProvider(syncId: 1).notifier, - ) - .addChangedPart( - ActionType.updateItem, - manga.id, - manga.toJson(), - false, - ); + 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.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; + isar.mangas.putSync(widget.oldManga); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart( + ActionType.updateItem, + widget.oldManga.id, + widget.oldManga.toJson(), + false, + ); + }); + 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); } } - }); - } - if (downloadedChapsList.isNotEmpty) { - isar.writeTxnSync(() async { - for (var manga in mangasList) { - if (manga.isLocalArchive ?? false) { - for (var chapter in manga.chapters) { - try { - final storageProvider = - StorageProvider(); - final mangaDir = await storageProvider - .getMangaMainDirectory(chapter); - final path = await storageProvider - .getMangaChapterDirectory( - chapter, - mangaMainDirectory: mangaDir, - ); - - try { - try { - if (File( - "${mangaDir!.path}${chapter.name}.cbz", - ).existsSync()) { - File( - "${mangaDir.path}${chapter.name}.cbz", - ).deleteSync(); - } - } catch (_) {} - try { - if (File( - "${mangaDir!.path}${chapter.name}.mp4", - ).existsSync()) { - File( - "${mangaDir.path}${chapter.name}.mp4", - ).deleteSync(); - } - } catch (_) {} - path!.deleteSync(recursive: true); - } catch (_) {} - isar.writeTxnSync(() { - final download = isar.downloads - .filter() - .idEqualTo(chapter.id!) - .findAllSync(); - if (download.isNotEmpty) { - isar.downloads.deleteSync( - download.first.id!, - ); - } - }); - } catch (_) {} - } - } + 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.read(mangasListStateProvider.notifier).clear(); - ref - .read(isLongPressedMangaStateProvider.notifier) - .update(false); - if (mounted) { - Navigator.pop(context); - } - }, - child: Text(l10n.ok), + ref.invalidate( + getMangaDetailStreamProvider( + mangaId: widget.oldManga.id!, + ), + ); + if (ctx.mounted) { + Navigator.pop(ctx); + Navigator.pop(ctx); + } + }, + child: Text(l10n.ok), + ), ), ], ), @@ -1547,740 +542,25 @@ class _TrackerLibraryScreenState extends ConsumerState ); }, ); - }, - ); - }, - ); + } + }); } - 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 mangas, - bool isCategory, - int? categoryId, - Settings settings, - ) { - final isLongPressed = ref.watch(isLongPressedMangaStateProvider); - 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(isLongPressedMangaStateProvider.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(isLongPressedMangaStateProvider.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 == ItemType.manga - ? l10n.manga - : widget.itemType == ItemType.anime - ? l10n.anime - : l10n.novel, - 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 aren’t 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( - value: 0, - child: Text(context.l10n.update_library), - ), - PopupMenuItem( - value: 1, - child: Text(l10n.open_random_entry), - ), - ]; - }, - onSelected: (value) { - if (value == 0) { - manga.whenData((value) { - _updateLibrary(value); - }); - } 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!, - ); - }); - } - }, - ), - ], - ), - ); + 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; } } diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index 7ad67f6b..792f9e3d 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -149,13 +149,16 @@ class MyAnimeList extends _$MyAnimeList { ); } - Future> getGlobalData(bool isManga) async { + Future> fetchData({ + bool isManga = true, + String rankingType = "airing", // bypopularity, tv, upcoming - all, manga, manhwa, manhua + }) async { final accessToken = await _getAccessToken(); final item = isManga ? "manga" : "anime"; final contentUnit = isManga ? "num_chapters" : "num_episodes"; final url = Uri.parse('$baseApiUrl/$item/ranking').replace( queryParameters: { - 'ranking_type': 'airing', + 'ranking_type': rankingType, 'limit': '15', 'fields': 'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date,mean', From 51352efd24391ac5759a05062fe564153066aa9e Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Fri, 6 Jun 2025 00:22:31 +0200 Subject: [PATCH 5/8] + --- .../providers/track_state_providers.dart | 10 +++- lib/services/trackers/myanimelist.dart | 55 ++++++++++++++++++- lib/services/trackers/trakt.dart | 3 +- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/lib/modules/manga/detail/providers/track_state_providers.dart b/lib/modules/manga/detail/providers/track_state_providers.dart index 833550b2..c1d5cce1 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.dart @@ -139,9 +139,15 @@ class TrackState extends _$TrackState { return await tracker.search(query, _isManga); } - Future?> fetchData({String rankingType = "airing"}) async { + Future?> fetchGeneralData({String rankingType = "airing"}) async { final syncId = track!.syncId!; final tracker = getNotifier(syncId); - return await tracker.fetchData(_isManga, rankingType); + return await tracker.fetchGeneralData(_isManga, rankingType); + } + + Future?> fetchUserData() async { + final syncId = track!.syncId!; + final tracker = getNotifier(syncId); + return await tracker.fetchUserData(_isManga); } } diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index 792f9e3d..0c31c4d1 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -149,9 +149,10 @@ class MyAnimeList extends _$MyAnimeList { ); } - Future> fetchData({ + Future> fetchGeneralData({ bool isManga = true, - String rankingType = "airing", // bypopularity, tv, upcoming - all, manga, manhwa, manhua + String rankingType = + "airing", // bypopularity, tv, upcoming - all, manga, manhwa, manhua }) async { final accessToken = await _getAccessToken(); final item = isManga ? "manga" : "anime"; @@ -194,6 +195,56 @@ class MyAnimeList extends _$MyAnimeList { .toList(); } + Future> fetchUserData({bool isManga = true}) async { + final accessToken = await _getAccessToken(); + final item = isManga ? "mangalist" : "animelist"; + final contentUnit = isManga ? "num_chapters" : "num_episodes"; + final url = Uri.parse('$baseApiUrl/users/@me/$item').replace( + queryParameters: { + 'sort': 'list_updated_at', + 'limit': '1000', + 'fields': + 'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date,mean,list_status', + }, + ); + final result = await _makeGetRequest(url, accessToken); + final res = jsonDecode(result.body) as Map; + + return res['data'] == null + ? [] + : (res['data'] as List) + .map( + (e) => TrackSearch( + mediaId: e["node"]["id"], + summary: e["node"]["synopsis"] ?? "", + totalChapter: e["node"][contentUnit], + coverUrl: e["node"]["main_picture"]["large"] ?? "", + title: e["node"]["title"], + score: e["node"]["mean"], + startDate: e["node"]["start_date"] ?? "", + publishingType: e["node"]["media_type"].toString().replaceAll( + "_", + " ", + ), + publishingStatus: e["node"]["status"].toString().replaceAll( + "_", + " ", + ), + trackingUrl: + "https://myanimelist.net/$item/${e["node"]["id"]}", + startedReadingDate: e["list_status"]["start_date"], + finishedReadingDate: e["list_status"]["finish_date"], + lastChapterRead: + e["list_status"][isManga + ? "num_chapters_read" + : "num_episodes_watched"], + status: + e["list_status"]["status"], // TODO map status enum correctly + ), + ) + .toList(); + } + String _convertToIsoDate(int? epochTime) { String date = ""; try { diff --git a/lib/services/trackers/trakt.dart b/lib/services/trackers/trakt.dart index cec3db72..4412ac99 100644 --- a/lib/services/trackers/trakt.dart +++ b/lib/services/trackers/trakt.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +/*import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; @@ -312,3 +312,4 @@ class Trakt extends _$Trakt { ); } } +*/ \ No newline at end of file From 4c3b6bc4304537f260f8050d60251708e7189bcb Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Sun, 8 Jun 2025 03:02:18 +0200 Subject: [PATCH 6/8] + --- .../providers/track_state_providers.g.dart | 2 +- .../data_and_storage/providers/restore.g.dart | 2 +- .../tracker_library_screen.dart | 153 ++++++++++++------ lib/router/router.g.dart | 2 +- lib/services/aniskip.g.dart | 2 +- lib/services/trackers/anilist.g.dart | 2 +- lib/services/trackers/myanimelist.dart | 19 ++- lib/services/trackers/myanimelist.g.dart | 2 +- 8 files changed, 124 insertions(+), 60 deletions(-) diff --git a/lib/modules/manga/detail/providers/track_state_providers.g.dart b/lib/modules/manga/detail/providers/track_state_providers.g.dart index 4246bdc5..9fcd08a2 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.g.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.g.dart @@ -6,7 +6,7 @@ part of 'track_state_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$trackStateHash() => r'4d31a8a939412cabd800f9747bff7a1ac0ef1996'; +String _$trackStateHash() => r'67e97d5b4e253e4289799c5d43bfa96e7f2b5140'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/more/data_and_storage/providers/restore.g.dart b/lib/modules/more/data_and_storage/providers/restore.g.dart index 0fb6dc00..d75f9525 100644 --- a/lib/modules/more/data_and_storage/providers/restore.g.dart +++ b/lib/modules/more/data_and_storage/providers/restore.g.dart @@ -173,7 +173,7 @@ class _DoRestoreProviderElement extends AutoDisposeProviderElement BuildContext get context => (origin as DoRestoreProvider).context; } -String _$restoreBackupHash() => r'cd8cdf4a40009bb43a9e11ea2781efad2b8dd640'; +String _$restoreBackupHash() => r'd4226539077680dac8826ee9b36efff9b07e2ed8'; /// See also [restoreBackup]. @ProviderFor(restoreBackup) diff --git a/lib/modules/tracker_library/tracker_library_screen.dart b/lib/modules/tracker_library/tracker_library_screen.dart index 317fd783..05eb315e 100644 --- a/lib/modules/tracker_library/tracker_library_screen.dart +++ b/lib/modules/tracker_library/tracker_library_screen.dart @@ -17,7 +17,10 @@ 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/track.dart'; +import 'package:mangayomi/models/track_search.dart'; import 'package:mangayomi/models/update.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/more/categories/providers/isar_providers.dart'; import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; @@ -46,68 +49,126 @@ enum TrackerProviders { kitsu(syncId: 3), trakt(syncId: 4); - const TrackerProviders({ - required this.syncId, - }); + const TrackerProviders({required this.syncId}); final int syncId; } +class TrackLibrarySection { + String name; + Future?> Function() func; + + TrackLibrarySection({required this.name, required this.func}); +} + class TrackerLibraryScreen extends ConsumerStatefulWidget { final TrackerProviders trackerProvider; const TrackerLibraryScreen({required this.trackerProvider, super.key}); @override - ConsumerState createState() => _TrackerLibraryScreenState(); + ConsumerState createState() => + _TrackerLibraryScreenState(); } class _TrackerLibraryScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final sections = [ + TrackLibrarySection( + name: "Airing Anime", + func: fetchGeneralData(ItemType.anime), + ), + TrackLibrarySection( + name: "Popular Anime", + func: fetchGeneralData(ItemType.anime, rankingType: "bypopularity"), + ), + TrackLibrarySection( + name: "Upcoming Anime", + func: fetchGeneralData(ItemType.anime, rankingType: "upcoming"), + ), + TrackLibrarySection( + name: "Airing Manga", + func: fetchGeneralData(ItemType.manga), + ), + TrackLibrarySection( + name: "Popular Manga", + func: fetchGeneralData(ItemType.manga, rankingType: "bypopularity"), + ), + TrackLibrarySection( + name: "Upcoming Manga", + func: fetchGeneralData(ItemType.manga, rankingType: "upcoming"), + ), + TrackLibrarySection( + name: "Continue watching", + func: fetchUserData(ItemType.anime), + ), + TrackLibrarySection( + name: "Continue reading", + func: fetchUserData(ItemType.manga), + ), + ]; final l10n = l10nLocalizations(context)!; return Scaffold( appBar: AppBar(title: Text(l10n.migrate)), - body: 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( - query: widget.manga.name ?? widget.manga.author ?? "", - manga: widget.manga, - source: source, - ), - ); - }, - ) - : Container(), + body: SuperListView.builder( + itemCount: sections.length, + extentPrecalculationPolicy: SuperPrecalculationPolicy(), + itemBuilder: (context, index) { + final section = sections[index]; + return SizedBox( + height: 260, + child: TrackerSectionScreen( + section: section, + ), + ); + }, + ), ); } + + Future?> Function() fetchGeneralData( + ItemType itemType, { + String rankingType = "airing", + }) { + return () async => await ref + .read( + trackStateProvider( + track: Track( + syncId: widget.trackerProvider.syncId, + status: TrackStatus.completed, + ), + itemType: itemType, + ).notifier, + ) + .fetchGeneralData(rankingType: rankingType); + } + + Future?> Function() fetchUserData(ItemType itemType) { + return () async => await ref + .read( + trackStateProvider( + track: Track( + syncId: widget.trackerProvider.syncId, + status: TrackStatus.completed, + ), + itemType: itemType, + ).notifier, + ) + .fetchUserData(); + } } -class MigrationSourceSearchScreen extends StatefulWidget { - final String query; - final Manga manga; +class TrackerSectionScreen extends StatefulWidget { + final TrackLibrarySection section; - final Source source; - const MigrationSourceSearchScreen({ - super.key, - required this.query, - required this.manga, - required this.source, - }); + const TrackerSectionScreen({super.key, required this.section}); @override - State createState() => - _MigrationSourceSearchScreenState(); + State createState() => _TrackerSectionScreenState(); } -class _MigrationSourceSearchScreenState - extends State { +class _TrackerSectionScreenState extends State { @override void initState() { super.initState(); @@ -116,16 +177,11 @@ class _MigrationSourceSearchScreenState String _errorMessage = ""; bool _isLoading = true; - MPages? pages; + List tracks = []; _init() async { try { _errorMessage = ""; - pages = await search( - source: widget.source, - page: 1, - query: widget.query, - filterList: [], - ); + tracks = await widget.section.func() ?? []; if (mounted) { setState(() { _isLoading = false; @@ -150,14 +206,7 @@ class _MigrationSourceSearchScreenState height: 260, child: Column( children: [ - ListTile( - dense: true, - title: Text(widget.source.name!), - subtitle: Text( - completeLanguageName(widget.source.lang!), - style: const TextStyle(fontSize: 10), - ), - ), + ListTile(dense: true, title: Text(widget.section.name)), Flexible( child: _isLoading ? const Center(child: CircularProgressIndicator()) @@ -166,12 +215,12 @@ class _MigrationSourceSearchScreenState if (_errorMessage.isNotEmpty) { return Center(child: Text(_errorMessage)); } - if (pages!.list.isNotEmpty) { + if (tracks.isNotEmpty) { return SuperListView.builder( extentPrecalculationPolicy: SuperPrecalculationPolicy(), scrollDirection: Axis.horizontal, - itemCount: pages!.list.length, + itemCount: tracks.length, itemBuilder: (context, index) { return MigrationMangaGlobalImageCard( oldManga: widget.manga, diff --git a/lib/router/router.g.dart b/lib/router/router.g.dart index a89d7773..97d99bd8 100644 --- a/lib/router/router.g.dart +++ b/lib/router/router.g.dart @@ -23,7 +23,7 @@ final routerProvider = AutoDisposeProvider.internal( // ignore: unused_element typedef RouterRef = AutoDisposeProviderRef; String _$routerCurrentLocationStateHash() => - r'3d04109aaeded1b2255b1f1d05abe2819fe694de'; + r'e3248e9cbfd34a1c0576b28d9bcc01a07f644cb9'; /// See also [RouterCurrentLocationState]. @ProviderFor(RouterCurrentLocationState) diff --git a/lib/services/aniskip.g.dart b/lib/services/aniskip.g.dart index b2e1defb..c05db920 100644 --- a/lib/services/aniskip.g.dart +++ b/lib/services/aniskip.g.dart @@ -6,7 +6,7 @@ part of 'aniskip.dart'; // RiverpodGenerator // ************************************************************************** -String _$aniSkipHash() => r'887869b54e2e151633efd46da83bde845e14f421'; +String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c'; /// See also [AniSkip]. @ProviderFor(AniSkip) diff --git a/lib/services/trackers/anilist.g.dart b/lib/services/trackers/anilist.g.dart index 40610576..703c90a8 100644 --- a/lib/services/trackers/anilist.g.dart +++ b/lib/services/trackers/anilist.g.dart @@ -6,7 +6,7 @@ part of 'anilist.dart'; // RiverpodGenerator // ************************************************************************** -String _$anilistHash() => r'80c9c6e9028e8a8180795366729acbe6c248d9ce'; +String _$anilistHash() => r'd672e47052f0b40088dd477b7918dc1e06654b48'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index 0c31c4d1..30e577fd 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -238,8 +238,10 @@ class MyAnimeList extends _$MyAnimeList { e["list_status"][isManga ? "num_chapters_read" : "num_episodes_watched"], - status: - e["list_status"]["status"], // TODO map status enum correctly + status: fromMyAnimeListStatus( + e["list_status"]["status"], + isManga, + ).name, ), ) .toList(); @@ -303,6 +305,19 @@ class MyAnimeList extends _$MyAnimeList { }; } + TrackStatus fromMyAnimeListStatus(String status, bool isManga) { + return switch (status) { + "reading" when isManga => TrackStatus.reading, + "watching" when !isManga => TrackStatus.watching, + "completed" => TrackStatus.completed, + "on_hold" => TrackStatus.onHold, + "dropped" => TrackStatus.dropped, + "plan_to_read" when isManga => TrackStatus.planToRead, + "plan_to_watch" when !isManga => TrackStatus.planToWatch, + _ => isManga ? TrackStatus.reading : TrackStatus.planToWatch, + }; + } + Future _getOAuth(String code) async { final params = { 'client_id': clientId, diff --git a/lib/services/trackers/myanimelist.g.dart b/lib/services/trackers/myanimelist.g.dart index 8d0071b1..8ae80e5e 100644 --- a/lib/services/trackers/myanimelist.g.dart +++ b/lib/services/trackers/myanimelist.g.dart @@ -6,7 +6,7 @@ part of 'myanimelist.dart'; // RiverpodGenerator // ************************************************************************** -String _$myAnimeListHash() => r'a7d644ee61119350613a9cff2fbe87dbd2f98912'; +String _$myAnimeListHash() => r'ecf9947e37eb0485fcc015ce002d9a440a0ecee1'; /// Copied from Dart SDK class _SystemHash { From e994d3230a3eb541f843b822c7623a49e83b9971 Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Sun, 8 Jun 2025 19:11:31 +0200 Subject: [PATCH 7/8] + --- lib/main.dart | 10 + lib/modules/main_view/main_screen.dart | 67 +++ .../providers/track_state_providers.dart | 11 +- .../providers/track_state_providers.g.dart | 2 +- .../appearance/appearance_screen.dart | 3 + .../providers/reader_state_provider.dart | 40 +- .../providers/reader_state_provider.g.dart | 4 +- .../tracker_library_screen.dart | 436 +++++------------- lib/router/router.dart | 4 +- lib/services/trackers/myanimelist.dart | 12 +- lib/services/trackers/myanimelist.g.dart | 2 +- 11 files changed, 235 insertions(+), 356 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 5fb98a31..fb084e3c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:app_links/app_links.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -117,6 +118,7 @@ class _MyAppState extends ConsumerState { routerDelegate: router.routerDelegate, routeInformationProvider: router.routeInformationProvider, title: 'MangaYomi', + scrollBehavior: AllowDesktopScrollBehavior(), ); } @@ -230,3 +232,11 @@ class _MyAppState extends ConsumerState { return true; } } + +class AllowDesktopScrollBehavior extends MaterialScrollBehavior { + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} diff --git a/lib/modules/main_view/main_screen.dart b/lib/modules/main_view/main_screen.dart index 0dea3107..25689d46 100644 --- a/lib/modules/main_view/main_screen.dart +++ b/lib/modules/main_view/main_screen.dart @@ -325,6 +325,42 @@ class _MainScreenState extends ConsumerState { ), ); } + if (dest.contains("/trackerLibrary/anilist")) { + destinations[dest.indexOf( + "/trackerLibrary/anilist", + )] = NavigationRailDestination( + selectedIcon: const Icon(Icons.account_tree), + icon: const Icon(Icons.account_tree_outlined), + label: Padding( + padding: const EdgeInsets.only(top: 5), + child: Text("AL"), + ), + ); + } + if (dest.contains("/trackerLibrary/kitsu")) { + destinations[dest.indexOf( + "/trackerLibrary/kitsu", + )] = NavigationRailDestination( + selectedIcon: const Icon(Icons.account_tree), + icon: const Icon(Icons.account_tree_outlined), + label: Padding( + padding: const EdgeInsets.only(top: 5), + child: Text("Kitsu"), + ), + ); + } + if (dest.contains("/trackerLibrary/mal")) { + destinations[dest.indexOf( + "/trackerLibrary/mal", + )] = NavigationRailDestination( + selectedIcon: const Icon(Icons.account_tree), + icon: const Icon(Icons.account_tree_outlined), + label: Padding( + padding: const EdgeInsets.only(top: 5), + child: Text("MAL"), + ), + ); + } final result = destinations.nonNulls.toList(); _desktopDestinationsCache[cacheKey] = result; @@ -408,6 +444,31 @@ class _MainScreenState extends ConsumerState { label: l10n.more, ); } + if (dest.contains("/trackerLibrary/anilist")) { + destinations[dest.indexOf( + "/trackerLibrary/anilist", + )] = NavigationDestination( + selectedIcon: const Icon(Icons.account_tree), + icon: const Icon(Icons.account_tree_outlined), + label: "AL", + ); + } + if (dest.contains("/trackerLibrary/kitsu")) { + destinations[dest.indexOf( + "/trackerLibrary/kitsu", + )] = NavigationDestination( + selectedIcon: const Icon(Icons.account_tree), + icon: const Icon(Icons.account_tree_outlined), + label: "Kitsu", + ); + } + if (dest.contains("/trackerLibrary/mal")) { + destinations[dest.indexOf("/trackerLibrary/mal")] = NavigationDestination( + selectedIcon: const Icon(Icons.account_tree), + icon: const Icon(Icons.account_tree_outlined), + label: "MAL", + ); + } _mobileDestinationsCache[cacheKey] = destinations; return destinations; @@ -527,6 +588,9 @@ class _TabletLayout extends StatelessWidget { '/updates', '/browse', '/more', + '/trackerLibrary/anilist', + '/trackerLibrary/kitsu', + '/trackerLibrary/mal', }; return (location == null || validLocations.contains(location)) ? 100 : 0; @@ -594,6 +658,9 @@ class _MobileBottomNavigation extends StatelessWidget { '/updates', '/browse', '/more', + '/trackerLibrary/anilist', + '/trackerLibrary/kitsu', + '/trackerLibrary/mal', }; return (location == null || validLocations.contains(location)) ? null : 0; diff --git a/lib/modules/manga/detail/providers/track_state_providers.dart b/lib/modules/manga/detail/providers/track_state_providers.dart index c1d5cce1..d7f80ab5 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.dart @@ -139,15 +139,20 @@ class TrackState extends _$TrackState { return await tracker.search(query, _isManga); } - Future?> fetchGeneralData({String rankingType = "airing"}) async { + Future?> fetchGeneralData({ + String rankingType = "airing", + }) async { final syncId = track!.syncId!; final tracker = getNotifier(syncId); - return await tracker.fetchGeneralData(_isManga, rankingType); + return await tracker.fetchGeneralData( + isManga: _isManga, + rankingType: rankingType, + ); } Future?> fetchUserData() async { final syncId = track!.syncId!; final tracker = getNotifier(syncId); - return await tracker.fetchUserData(_isManga); + return await tracker.fetchUserData(isManga: _isManga); } } diff --git a/lib/modules/manga/detail/providers/track_state_providers.g.dart b/lib/modules/manga/detail/providers/track_state_providers.g.dart index 9fcd08a2..718848a6 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.g.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.g.dart @@ -6,7 +6,7 @@ part of 'track_state_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$trackStateHash() => r'67e97d5b4e253e4289799c5d43bfa96e7f2b5140'; +String _$trackStateHash() => r'b10c02c2e50eb1f044a76560093a8dcf232487c5'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/more/settings/appearance/appearance_screen.dart b/lib/modules/more/settings/appearance/appearance_screen.dart index 9c83cc6d..2474f357 100644 --- a/lib/modules/more/settings/appearance/appearance_screen.dart +++ b/lib/modules/more/settings/appearance/appearance_screen.dart @@ -25,6 +25,9 @@ final navigationItems = { "/history": "History", "/browse": "Browse", "/more": "More", + "/trackerLibrary/anilist": "AL", + "/trackerLibrary/kitsu": "Kitsu", + "/trackerLibrary/mal": "MAL", }; class SettingsSection extends StatelessWidget { diff --git a/lib/modules/more/settings/reader/providers/reader_state_provider.dart b/lib/modules/more/settings/reader/providers/reader_state_provider.dart index 753fa984..595afd64 100644 --- a/lib/modules/more/settings/reader/providers/reader_state_provider.dart +++ b/lib/modules/more/settings/reader/providers/reader_state_provider.dart @@ -149,18 +149,31 @@ class FullScreenReaderState extends _$FullScreenReaderState { @riverpod class NavigationOrderState extends _$NavigationOrderState { + final items = [ + '/MangaLibrary', + '/AnimeLibrary', + '/NovelLibrary', + '/updates', + '/history', + '/browse', + '/more', + '/trackerLibrary/anilist', + '/trackerLibrary/kitsu', + '/trackerLibrary/mal', + ]; + @override List build() { - return isar.settings.getSync(227)!.navigationOrder ?? - [ - '/MangaLibrary', - '/AnimeLibrary', - '/NovelLibrary', - '/updates', - '/history', - '/browse', - '/more', - ]; + return _checkMissingItems( + isar.settings.getSync(227)!.navigationOrder?.toList() ?? [], + ); + } + + List _checkMissingItems(List navigationOrder) { + navigationOrder.addAll( + items.where((e) => !navigationOrder.contains(e)).toList(), + ); + return navigationOrder; } void set(List values) { @@ -176,7 +189,12 @@ class NavigationOrderState extends _$NavigationOrderState { class HideItemsState extends _$HideItemsState { @override List build() { - return isar.settings.getSync(227)!.hideItems ?? []; + return isar.settings.getSync(227)!.hideItems ?? + [ + '/trackerLibrary/anilist', + '/trackerLibrary/kitsu', + '/trackerLibrary/mal', + ]; } void set(List values) { diff --git a/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart b/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart index 3b672a06..b86397cb 100644 --- a/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart +++ b/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart @@ -158,7 +158,7 @@ final fullScreenReaderStateProvider = typedef _$FullScreenReaderState = AutoDisposeNotifier; String _$navigationOrderStateHash() => - r'f1da55a7687995d136a6580d3f63f9b1b32a6ae8'; + r'f300869743afaccfd47210115f341d25fec522bb'; /// See also [NavigationOrderState]. @ProviderFor(NavigationOrderState) @@ -174,7 +174,7 @@ final navigationOrderStateProvider = ); typedef _$NavigationOrderState = AutoDisposeNotifier>; -String _$hideItemsStateHash() => r'b4a467e66f6a1f9b36e4b201a10b771e0dae6a80'; +String _$hideItemsStateHash() => r'6844a05786f6c547a7cba261f742e82d871b6cb1'; /// See also [HideItemsState]. @ProviderFor(HideItemsState) diff --git a/lib/modules/tracker_library/tracker_library_screen.dart b/lib/modules/tracker_library/tracker_library_screen.dart index 05eb315e..bb82f0c2 100644 --- a/lib/modules/tracker_library/tracker_library_screen.dart +++ b/lib/modules/tracker_library/tracker_library_screen.dart @@ -1,69 +1,55 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:io'; -import 'dart:math'; -import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:isar/isar.dart'; -import 'package:mangayomi/eval/model/m_bridge.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/download.dart'; -import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; -import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/models/track_search.dart'; -import 'package:mangayomi/models/update.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/more/categories/providers/isar_providers.dart'; -import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; -import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; -import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart'; -import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart'; +import 'package:mangayomi/modules/widgets/bottom_text_widget.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; -import 'package:mangayomi/providers/storage_provider.dart'; +import 'package:mangayomi/utils/cached_network.dart'; +import 'package:mangayomi/utils/constant.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/library/widgets/list_tile_manga_category.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/global_style.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; enum TrackerProviders { - anilist(syncId: 1), - myAnimeList(syncId: 2), - kitsu(syncId: 3), - trakt(syncId: 4); + myAnimeList(syncId: 1, name: "MAL"), + anilist(syncId: 2, name: "AL"), + kitsu(syncId: 3, name: "Kitsu"), + trakt(syncId: 4, name: "Trakt"); - const TrackerProviders({required this.syncId}); + const TrackerProviders({required this.syncId, required this.name}); final int syncId; + final String name; } class TrackLibrarySection { String name; Future?> Function() func; + ItemType itemType; - TrackLibrarySection({required this.name, required this.func}); + TrackLibrarySection({ + required this.name, + required this.func, + this.itemType = ItemType.manga, + }); } class TrackerLibraryScreen extends ConsumerStatefulWidget { final TrackerProviders trackerProvider; - const TrackerLibraryScreen({required this.trackerProvider, super.key}); + final String? presetInput; + const TrackerLibraryScreen({ + required this.trackerProvider, + required this.presetInput, + super.key, + }); @override ConsumerState createState() => @@ -77,52 +63,60 @@ class _TrackerLibraryScreenState extends ConsumerState { TrackLibrarySection( name: "Airing Anime", func: fetchGeneralData(ItemType.anime), + itemType: ItemType.anime, ), TrackLibrarySection( name: "Popular Anime", func: fetchGeneralData(ItemType.anime, rankingType: "bypopularity"), + itemType: ItemType.anime, ), TrackLibrarySection( name: "Upcoming Anime", func: fetchGeneralData(ItemType.anime, rankingType: "upcoming"), + itemType: ItemType.anime, ), TrackLibrarySection( - name: "Airing Manga", - func: fetchGeneralData(ItemType.manga), + name: "Continue watching", + func: fetchUserData(ItemType.anime), + itemType: ItemType.anime, ), TrackLibrarySection( name: "Popular Manga", func: fetchGeneralData(ItemType.manga, rankingType: "bypopularity"), ), TrackLibrarySection( - name: "Upcoming Manga", - func: fetchGeneralData(ItemType.manga, rankingType: "upcoming"), + name: "Top Manga", + func: fetchGeneralData(ItemType.manga, rankingType: "manga"), ), TrackLibrarySection( - name: "Continue watching", - func: fetchUserData(ItemType.anime), + name: "Top Manhwa", + func: fetchGeneralData(ItemType.manga, rankingType: "manhwa"), + ), + TrackLibrarySection( + name: "Top Manhua ", + func: fetchGeneralData(ItemType.manga, rankingType: "manhua"), ), TrackLibrarySection( name: "Continue reading", func: fetchUserData(ItemType.manga), ), ]; - final l10n = l10nLocalizations(context)!; return Scaffold( - appBar: AppBar(title: Text(l10n.migrate)), - body: SuperListView.builder( - itemCount: sections.length, - extentPrecalculationPolicy: SuperPrecalculationPolicy(), - itemBuilder: (context, index) { - final section = sections[index]; - return SizedBox( - height: 260, - child: TrackerSectionScreen( - section: section, - ), - ); - }, + appBar: AppBar(title: Text(widget.trackerProvider.name)), + body: Padding( + padding: const EdgeInsets.all(15), + child: SuperListView.builder( + itemCount: sections.length, + extentPrecalculationPolicy: SuperPrecalculationPolicy(), + itemBuilder: (context, index) { + final section = sections[index]; + return SizedBox( + height: 260, + child: TrackerSectionScreen(section: section), + ); + }, + ), ), ); } @@ -222,10 +216,9 @@ class _TrackerSectionScreenState extends State { scrollDirection: Axis.horizontal, itemCount: tracks.length, itemBuilder: (context, index) { - return MigrationMangaGlobalImageCard( - oldManga: widget.manga, - manga: pages!.list[index], - source: widget.source, + return TrackerLibraryImageCard( + track: tracks[index], + itemType: widget.section.itemType, ); }, ); @@ -241,39 +234,35 @@ class _TrackerSectionScreenState extends State { } } -class MigrationMangaGlobalImageCard extends ConsumerStatefulWidget { - final Manga oldManga; - final MManga manga; - final Source source; +class TrackerLibraryImageCard extends ConsumerStatefulWidget { + final TrackSearch track; + final ItemType itemType; - const MigrationMangaGlobalImageCard({ + const TrackerLibraryImageCard({ super.key, - required this.oldManga, - required this.manga, - required this.source, + required this.track, + required this.itemType, }); @override - ConsumerState createState() => - _MigrationMangaGlobalImageCardState(); + ConsumerState createState() => + _TrackerLibraryImageCardState(); } -class _MigrationMangaGlobalImageCardState - extends ConsumerState - with AutomaticKeepAliveClientMixin { +class _TrackerLibraryImageCardState + extends ConsumerState + with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - final l10n = l10nLocalizations(context)!; - final getMangaDetail = widget.manga; + final trackData = widget.track; return GestureDetector( - onTap: () => _showMigrateDialog(context, l10n), + onTap: () => _pushMigrationScreen(context), child: StreamBuilder( stream: isar.mangas .filter() - .langEqualTo(widget.source.lang) - .nameEqualTo(getMangaDetail.name) - .sourceEqualTo(widget.source.name) + .itemTypeEqualTo(widget.itemType) + .nameEqualTo(trackData.title) .watch(fireImmediately: true), builder: (context, snapshot) { final hasData = snapshot.hasData && snapshot.data!.isNotEmpty; @@ -297,12 +286,6 @@ class _MigrationMangaGlobalImageCardState return ClipRRect( borderRadius: BorderRadius.circular(5), child: cachedNetworkImage( - headers: ref.watch( - headersProvider( - source: widget.source.name!, - lang: widget.source.lang!, - ), - ), imageUrl: toImgUrl( hasData ? snapshot @@ -311,7 +294,7 @@ class _MigrationMangaGlobalImageCardState .customCoverFromTracker ?? snapshot.data!.first.imageUrl ?? "" - : getMangaDetail.imageUrl ?? "", + : trackData.coverUrl ?? "", ), width: 110, height: 150, @@ -322,7 +305,7 @@ class _MigrationMangaGlobalImageCardState ), BottomTextWidget( fontSize: 12.0, - text: widget.manga.name!, + text: trackData.title!, isLoading: true, textColor: Theme.of(context).textTheme.bodyLarge!.color, isComfortableGrid: true, @@ -349,6 +332,20 @@ class _MigrationMangaGlobalImageCardState ), ), ), + Positioned( + bottom: 0, + left: 0, + child: Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Icon(Icons.star, color: context.primaryColor), + ), + TextSpan(text: " ${trackData.score ?? "?"}"), + ], + ), + ), + ), ], ), ); @@ -357,254 +354,27 @@ class _MigrationMangaGlobalImageCardState ); } + void _pushMigrationScreen(BuildContext context) { + context.push( + "/migrate", + extra: Manga( + name: widget.track.title, + itemType: widget.itemType, + source: null, + author: null, + artist: null, + genre: [], + imageUrl: null, + lang: null, + link: null, + status: Status.unknown, + description: null, + ), + ); + } + @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(l10n.migrate_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 { - String? historyChapter; - String? historyDate; - List 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.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; - isar.mangas.putSync(widget.oldManga); - ref - .read(synchingProvider(syncId: 1).notifier) - .addChangedPart( - ActionType.updateItem, - widget.oldManga.id, - widget.oldManga.toJson(), - false, - ); - }); - 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!, - ), - ); - if (ctx.mounted) { - Navigator.pop(ctx); - Navigator.pop(ctx); - } - }, - child: Text(l10n.ok), - ), - ), - ], - ), - ], - ); - }, - ); - } - }); - } - - 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 { diff --git a/lib/router/router.dart b/lib/router/router.dart index d324f246..59d3487f 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -123,8 +123,6 @@ class RouterNotifier extends ChangeNotifier { builder: (id) => LibraryScreen(itemType: ItemType.novel, presetInput: id), ), - _genericRoute(name: "history", child: const HistoryScreen()), - _genericRoute(name: "updates", child: const UpdatesScreen()), _genericRoute( name: "trackerLibrary/anilist", builder: (id) => TrackerLibraryScreen( @@ -146,6 +144,8 @@ class RouterNotifier extends ChangeNotifier { presetInput: id, ), ), + _genericRoute(name: "history", child: const HistoryScreen()), + _genericRoute(name: "updates", child: const UpdatesScreen()), _genericRoute(name: "browse", child: const BrowseScreen()), _genericRoute(name: "more", child: const MoreScreen()), ], diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index 30e577fd..a2484154 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -220,7 +220,9 @@ class MyAnimeList extends _$MyAnimeList { totalChapter: e["node"][contentUnit], coverUrl: e["node"]["main_picture"]["large"] ?? "", title: e["node"]["title"], - score: e["node"]["mean"], + score: e["node"]["mean"] is double + ? e["node"]["mean"] + : (e["node"]["mean"] as int).toDouble(), startDate: e["node"]["start_date"] ?? "", publishingType: e["node"]["media_type"].toString().replaceAll( "_", @@ -232,8 +234,12 @@ class MyAnimeList extends _$MyAnimeList { ), trackingUrl: "https://myanimelist.net/$item/${e["node"]["id"]}", - startedReadingDate: e["list_status"]["start_date"], - finishedReadingDate: e["list_status"]["finish_date"], + startedReadingDate: _parseDate( + e["list_status"]["start_date"], + ), + finishedReadingDate: _parseDate( + e["list_status"]["finish_date"], + ), lastChapterRead: e["list_status"][isManga ? "num_chapters_read" diff --git a/lib/services/trackers/myanimelist.g.dart b/lib/services/trackers/myanimelist.g.dart index 8ae80e5e..cd13b058 100644 --- a/lib/services/trackers/myanimelist.g.dart +++ b/lib/services/trackers/myanimelist.g.dart @@ -6,7 +6,7 @@ part of 'myanimelist.dart'; // RiverpodGenerator // ************************************************************************** -String _$myAnimeListHash() => r'ecf9947e37eb0485fcc015ce002d9a440a0ecee1'; +String _$myAnimeListHash() => r'eb483b6451d34e595bb770eefc0f673df13275b3'; /// Copied from Dart SDK class _SystemHash { From f7a124d72c5f1f17caf4b8250c0828e7b3e94468 Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Mon, 9 Jun 2025 02:22:33 +0200 Subject: [PATCH 8/8] added two-way-tracking --- lib/l10n/app_en.arb | 6 +- lib/l10n/generated/app_localizations.dart | 12 + lib/l10n/generated/app_localizations_ar.dart | 6 + lib/l10n/generated/app_localizations_de.dart | 6 + lib/l10n/generated/app_localizations_en.dart | 6 + lib/l10n/generated/app_localizations_es.dart | 6 + lib/l10n/generated/app_localizations_fr.dart | 6 + lib/l10n/generated/app_localizations_id.dart | 6 + lib/l10n/generated/app_localizations_it.dart | 6 + lib/l10n/generated/app_localizations_pt.dart | 6 + lib/l10n/generated/app_localizations_ru.dart | 6 + lib/l10n/generated/app_localizations_th.dart | 6 + lib/l10n/generated/app_localizations_tr.dart | 6 + lib/l10n/generated/app_localizations_zh.dart | 6 + .../manga/detail/widgets/migrate_screen.dart | 396 ++++++++++++------ .../tracker_library_screen.dart | 30 +- lib/router/router.dart | 5 + lib/services/trackers/myanimelist.dart | 2 + 18 files changed, 377 insertions(+), 146 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fb35fd70..3487a087 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -472,7 +472,7 @@ "continue_to_next_chapter": "Continue scrolling to read the next chapter", "no_next_chapter": "No next chapter", "you_have_finished_reading": "You have finished reading", - "return_to_the_list_of_chapters": "Return to the list of chapters" - - + "return_to_the_list_of_chapters": "Return to the list of chapters", + "track_library_add": "Add to local library", + "track_library_add_confirm": "Add tracked item to local library" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 685ad543..1e5ff968 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2957,6 +2957,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Return to the list of chapters'** String get return_to_the_list_of_chapters; + + /// No description provided for @track_library_add. + /// + /// In en, this message translates to: + /// **'Add to local library'** + String get track_library_add; + + /// No description provided for @track_library_add_confirm. + /// + /// In en, this message translates to: + /// **'Add tracked item to local library'** + String get track_library_add_confirm; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 096b77ec..58a32dac 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1511,4 +1511,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index f4aaac89..1e0934d6 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1522,4 +1522,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index f7ade695..6f348341 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1511,4 +1511,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 072c7217..691d5a06 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1527,6 +1527,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } /// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`). diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 33c800ea..c6e3599a 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1532,4 +1532,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Retournez à la liste des chapitres'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index a9b5f847..2e2f67a0 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1517,4 +1517,10 @@ class AppLocalizationsId extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 0f4d4164..7d0c7978 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1526,4 +1526,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index b8ab9527..a7d09958 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1524,6 +1524,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } /// The translations for Portuguese, as used in Brazil (`pt_BR`). diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 633835a1..12cac872 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1526,4 +1526,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index fd18e120..b9ca8b48 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1512,4 +1512,10 @@ class AppLocalizationsTh extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 121466a8..a4579641 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1517,4 +1517,10 @@ class AppLocalizationsTr extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 1ccf1aa4..3c0b3d3c 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1480,4 +1480,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get return_to_the_list_of_chapters => 'Return to the list of chapters'; + + @override + String get track_library_add => 'Add to local library'; + + @override + String get track_library_add_confirm => 'Add tracked item to local library'; } diff --git a/lib/modules/manga/detail/widgets/migrate_screen.dart b/lib/modules/manga/detail/widgets/migrate_screen.dart index 8fe0ae40..13524e02 100644 --- a/lib/modules/manga/detail/widgets/migrate_screen.dart +++ b/lib/modules/manga/detail/widgets/migrate_screen.dart @@ -1,17 +1,22 @@ 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/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/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/sync/providers/sync_providers.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/models/source.dart'; @@ -28,7 +33,8 @@ import 'package:super_sliver_list/super_sliver_list.dart'; class MigrationScreen extends ConsumerStatefulWidget { final Manga manga; - const MigrationScreen({required this.manga, super.key}); + final TrackSearch? trackSearch; + const MigrationScreen({required this.manga, this.trackSearch, super.key}); @override ConsumerState createState() => _MigrationScreenScreenState(); @@ -48,7 +54,11 @@ class _MigrationScreenScreenState extends ConsumerState { final l10n = l10nLocalizations(context)!; return Scaffold( - appBar: AppBar(title: Text(l10n.migrate)), + appBar: AppBar( + title: Text( + widget.trackSearch == null ? l10n.migrate : l10n.track_library_add, + ), + ), body: widget.manga.name != null && widget.manga.author != null ? SuperListView.builder( itemCount: sourceList.length, @@ -61,6 +71,7 @@ class _MigrationScreenScreenState extends ConsumerState { query: widget.manga.name ?? widget.manga.author ?? "", manga: widget.manga, source: source, + trackSearch: widget.trackSearch, ), ); }, @@ -73,6 +84,7 @@ class _MigrationScreenScreenState extends ConsumerState { class MigrationSourceSearchScreen extends StatefulWidget { final String query; final Manga manga; + final TrackSearch? trackSearch; final Source source; const MigrationSourceSearchScreen({ @@ -80,6 +92,7 @@ class MigrationSourceSearchScreen extends StatefulWidget { required this.query, required this.manga, required this.source, + this.trackSearch, }); @override @@ -158,6 +171,7 @@ class _MigrationSourceSearchScreenState oldManga: widget.manga, manga: pages!.list[index], source: widget.source, + trackSearch: widget.trackSearch, ); }, ); @@ -177,12 +191,14 @@ 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 @@ -306,7 +322,11 @@ class _MigrationMangaGlobalImageCardState context: context, builder: (ctx) { return AlertDialog( - title: Text(l10n.migrate_confirm), + title: Text( + widget.trackSearch == null + ? l10n.migrate_confirm + : l10n.track_library_add_confirm, + ), content: preview.chapters != null ? SizedBox( height: ctx.height(0.5), @@ -385,133 +405,14 @@ class _MigrationMangaGlobalImageCardState Consumer( builder: (context, ref, child) => TextButton( onPressed: () async { - String? historyChapter; - String? historyDate; - List 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, - ); + if (widget.trackSearch == null) { + await _migrateManga(preview); + if (ctx.mounted) { + Navigator.pop(ctx); + Navigator.pop(ctx); } - 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.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; - isar.mangas.putSync(widget.oldManga); - ref - .read(synchingProvider(syncId: 1).notifier) - .addChangedPart( - ActionType.updateItem, - widget.oldManga.id, - widget.oldManga.toJson(), - false, - ); - }); - 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!, - ), - ); - if (ctx.mounted) { - Navigator.pop(ctx); - Navigator.pop(ctx); + } else { + await _addTrackManga(context); } }, child: Text(l10n.ok), @@ -527,6 +428,243 @@ class _MigrationMangaGlobalImageCardState }); } + Future _addTrackManga(BuildContext context) async { + List 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, + ); + int mangaId = -1; + isar.writeTxnSync(() { + mangaId = isar.mangas.putSync(manga); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart( + ActionType.addItem, + manga.id, + manga.toJson(), + false, + ); + }); + if (mangaId != -1) { + await ref + .read( + trackStateProvider( + track: null, + itemType: widget.oldManga.itemType, + ).notifier, + ) + .setTrackSearch( + widget.trackSearch!, + mangaId, + widget.trackSearch!.syncId!, + ); + } + if (context.mounted) { + Navigator.pop(context); + Navigator.pop(context); + } + }, + child: Text(l10n.ok), + ), + ], + ), + ], + ), + ], + ); + }, + ); + }, + ); + } + + Future _migrateManga(MManga preview) async { + String? historyChapter; + String? historyDate; + List 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.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; + isar.mangas.putSync(widget.oldManga); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart( + ActionType.updateItem, + widget.oldManga.id, + widget.oldManga.toJson(), + false, + ); + }); + 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*', diff --git a/lib/modules/tracker_library/tracker_library_screen.dart b/lib/modules/tracker_library/tracker_library_screen.dart index bb82f0c2..976c733f 100644 --- a/lib/modules/tracker_library/tracker_library_screen.dart +++ b/lib/modules/tracker_library/tracker_library_screen.dart @@ -1,7 +1,6 @@ // ignore_for_file: use_build_context_synchronously import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -356,19 +355,22 @@ class _TrackerLibraryImageCardState void _pushMigrationScreen(BuildContext context) { context.push( - "/migrate", - extra: Manga( - name: widget.track.title, - itemType: widget.itemType, - source: null, - author: null, - artist: null, - genre: [], - imageUrl: null, - lang: null, - link: null, - status: Status.unknown, - description: null, + "/migrate/tracker", + extra: ( + Manga( + name: widget.track.title, + itemType: widget.itemType, + source: null, + author: "", + artist: null, + genre: [], + imageUrl: null, + lang: null, + link: null, + status: Status.unknown, + description: null, + ), + widget.track, ), ); } diff --git a/lib/router/router.dart b/lib/router/router.dart index 59d3487f..bd89dd69 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/track_preference.dart'; +import 'package:mangayomi/models/track_search.dart'; import 'package:mangayomi/modules/anime/anime_player_view.dart'; import 'package:mangayomi/modules/browse/extension/edit_code.dart'; import 'package:mangayomi/modules/browse/extension/extension_detail.dart'; @@ -230,6 +231,10 @@ class RouterNotifier extends ChangeNotifier { name: "migrate", builder: (manga) => MigrationScreen(manga: manga), ), + _genericRoute<(Manga, TrackSearch)>( + name: "migrate/tracker", + builder: (data) => MigrationScreen(manga: data.$1, trackSearch: data.$2), + ), ]; GoRoute _genericRoute({ diff --git a/lib/services/trackers/myanimelist.dart b/lib/services/trackers/myanimelist.dart index a2484154..38d2d7e9 100644 --- a/lib/services/trackers/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -190,6 +190,7 @@ class MyAnimeList extends _$MyAnimeList { ), trackingUrl: "https://myanimelist.net/$item/${e["node"]["id"]}", + syncId: syncId, ), ) .toList(); @@ -248,6 +249,7 @@ class MyAnimeList extends _$MyAnimeList { e["list_status"]["status"], isManga, ).name, + syncId: syncId, ), ) .toList();