mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 16:01:58 +00:00
added two-way tracking
This commit is contained in:
commit
5dff4a1aa3
27 changed files with 1332 additions and 145 deletions
|
|
@ -474,5 +474,7 @@
|
||||||
"no_next_chapter": "No next chapter",
|
"no_next_chapter": "No next chapter",
|
||||||
"you_have_finished_reading": "You have finished reading",
|
"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",
|
||||||
"hwdec": "Hardware Decoder"
|
"hwdec": "Hardware Decoder",
|
||||||
|
"track_library_add": "Add to local library",
|
||||||
|
"track_library_add_confirm": "Add tracked item to local library"
|
||||||
}
|
}
|
||||||
|
|
@ -2969,6 +2969,18 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Hardware Decoder'**
|
/// **'Hardware Decoder'**
|
||||||
String get hwdec;
|
String get hwdec;
|
||||||
|
|
||||||
|
/// 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
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1517,4 +1517,10 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1528,4 +1528,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1517,4 +1517,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1534,6 +1534,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@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`).
|
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).
|
||||||
|
|
|
||||||
|
|
@ -1539,4 +1539,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1523,4 +1523,10 @@ class AppLocalizationsId extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1533,4 +1533,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1531,6 +1531,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@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`).
|
/// The translations for Portuguese, as used in Brazil (`pt_BR`).
|
||||||
|
|
|
||||||
|
|
@ -1533,4 +1533,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1518,4 +1518,10 @@ class AppLocalizationsTh extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1524,4 +1524,10 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1486,4 +1486,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hwdec => 'Hardware Decoder';
|
String get hwdec => 'Hardware Decoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add => 'Add to local library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get track_library_add_confirm => 'Add tracked item to local library';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:app_links/app_links.dart';
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -117,6 +118,7 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||||
routerDelegate: router.routerDelegate,
|
routerDelegate: router.routerDelegate,
|
||||||
routeInformationProvider: router.routeInformationProvider,
|
routeInformationProvider: router.routeInformationProvider,
|
||||||
title: 'MangaYomi',
|
title: 'MangaYomi',
|
||||||
|
scrollBehavior: AllowDesktopScrollBehavior(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,3 +232,11 @@ class _MyAppState extends ConsumerState<MyApp> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AllowDesktopScrollBehavior extends MaterialScrollBehavior {
|
||||||
|
@override
|
||||||
|
Set<PointerDeviceKind> get dragDevices => {
|
||||||
|
PointerDeviceKind.touch,
|
||||||
|
PointerDeviceKind.mouse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,42 @@ class _MainScreenState extends ConsumerState<MainScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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();
|
final result = destinations.nonNulls.toList();
|
||||||
_desktopDestinationsCache[cacheKey] = result;
|
_desktopDestinationsCache[cacheKey] = result;
|
||||||
|
|
@ -412,6 +448,31 @@ class _MainScreenState extends ConsumerState<MainScreen> {
|
||||||
label: l10n.more,
|
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;
|
_mobileDestinationsCache[cacheKey] = destinations;
|
||||||
return destinations;
|
return destinations;
|
||||||
|
|
@ -531,6 +592,9 @@ class _TabletLayout extends StatelessWidget {
|
||||||
'/updates',
|
'/updates',
|
||||||
'/browse',
|
'/browse',
|
||||||
'/more',
|
'/more',
|
||||||
|
'/trackerLibrary/anilist',
|
||||||
|
'/trackerLibrary/kitsu',
|
||||||
|
'/trackerLibrary/mal',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (location == null || validLocations.contains(location)) ? 100 : 0;
|
return (location == null || validLocations.contains(location)) ? 100 : 0;
|
||||||
|
|
@ -598,6 +662,9 @@ class _MobileBottomNavigation extends StatelessWidget {
|
||||||
'/updates',
|
'/updates',
|
||||||
'/browse',
|
'/browse',
|
||||||
'/more',
|
'/more',
|
||||||
|
'/trackerLibrary/anilist',
|
||||||
|
'/trackerLibrary/kitsu',
|
||||||
|
'/trackerLibrary/mal',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (location == null || validLocations.contains(location)) ? null : 0;
|
return (location == null || validLocations.contains(location)) ? null : 0;
|
||||||
|
|
|
||||||
|
|
@ -138,4 +138,21 @@ class TrackState extends _$TrackState {
|
||||||
final tracker = getNotifier(syncId);
|
final tracker = getNotifier(syncId);
|
||||||
return await tracker.search(query, _isManga);
|
return await tracker.search(query, _isManga);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<TrackSearch>?> fetchGeneralData({
|
||||||
|
String rankingType = "airing",
|
||||||
|
}) async {
|
||||||
|
final syncId = track!.syncId!;
|
||||||
|
final tracker = getNotifier(syncId);
|
||||||
|
return await tracker.fetchGeneralData(
|
||||||
|
isManga: _isManga,
|
||||||
|
rankingType: rankingType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TrackSearch>?> fetchUserData() async {
|
||||||
|
final syncId = track!.syncId!;
|
||||||
|
final tracker = getNotifier(syncId);
|
||||||
|
return await tracker.fetchUserData(isManga: _isManga);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'track_state_providers.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$trackStateHash() => r'4d31a8a939412cabd800f9747bff7a1ac0ef1996';
|
String _$trackStateHash() => r'b10c02c2e50eb1f044a76560093a8dcf232487c5';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||||
import 'package:mangayomi/main.dart';
|
import 'package:mangayomi/main.dart';
|
||||||
|
import 'package:mangayomi/models/category.dart';
|
||||||
import 'package:mangayomi/models/changed.dart';
|
import 'package:mangayomi/models/changed.dart';
|
||||||
import 'package:mangayomi/models/chapter.dart';
|
import 'package:mangayomi/models/chapter.dart';
|
||||||
import 'package:mangayomi/models/history.dart';
|
import 'package:mangayomi/models/history.dart';
|
||||||
import 'package:mangayomi/models/manga.dart';
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/track_search.dart';
|
||||||
import 'package:mangayomi/models/update.dart';
|
import 'package:mangayomi/models/update.dart';
|
||||||
import 'package:mangayomi/modules/manga/detail/providers/isar_providers.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/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/modules/more/settings/sync/providers/sync_providers.dart';
|
||||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||||
import 'package:mangayomi/models/source.dart';
|
import 'package:mangayomi/models/source.dart';
|
||||||
|
|
@ -28,7 +33,8 @@ import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
class MigrationScreen extends ConsumerStatefulWidget {
|
class MigrationScreen extends ConsumerStatefulWidget {
|
||||||
final Manga manga;
|
final Manga manga;
|
||||||
const MigrationScreen({required this.manga, super.key});
|
final TrackSearch? trackSearch;
|
||||||
|
const MigrationScreen({required this.manga, this.trackSearch, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<MigrationScreen> createState() => _MigrationScreenScreenState();
|
ConsumerState<MigrationScreen> createState() => _MigrationScreenScreenState();
|
||||||
|
|
@ -48,7 +54,11 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
||||||
final l10n = l10nLocalizations(context)!;
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
|
||||||
return Scaffold(
|
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
|
body: widget.manga.name != null && widget.manga.author != null
|
||||||
? SuperListView.builder(
|
? SuperListView.builder(
|
||||||
itemCount: sourceList.length,
|
itemCount: sourceList.length,
|
||||||
|
|
@ -61,6 +71,7 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
||||||
query: widget.manga.name ?? widget.manga.author ?? "",
|
query: widget.manga.name ?? widget.manga.author ?? "",
|
||||||
manga: widget.manga,
|
manga: widget.manga,
|
||||||
source: source,
|
source: source,
|
||||||
|
trackSearch: widget.trackSearch,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -73,6 +84,7 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
||||||
class MigrationSourceSearchScreen extends StatefulWidget {
|
class MigrationSourceSearchScreen extends StatefulWidget {
|
||||||
final String query;
|
final String query;
|
||||||
final Manga manga;
|
final Manga manga;
|
||||||
|
final TrackSearch? trackSearch;
|
||||||
|
|
||||||
final Source source;
|
final Source source;
|
||||||
const MigrationSourceSearchScreen({
|
const MigrationSourceSearchScreen({
|
||||||
|
|
@ -80,6 +92,7 @@ class MigrationSourceSearchScreen extends StatefulWidget {
|
||||||
required this.query,
|
required this.query,
|
||||||
required this.manga,
|
required this.manga,
|
||||||
required this.source,
|
required this.source,
|
||||||
|
this.trackSearch,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -158,6 +171,7 @@ class _MigrationSourceSearchScreenState
|
||||||
oldManga: widget.manga,
|
oldManga: widget.manga,
|
||||||
manga: pages!.list[index],
|
manga: pages!.list[index],
|
||||||
source: widget.source,
|
source: widget.source,
|
||||||
|
trackSearch: widget.trackSearch,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -177,12 +191,14 @@ class MigrationMangaGlobalImageCard extends ConsumerStatefulWidget {
|
||||||
final Manga oldManga;
|
final Manga oldManga;
|
||||||
final MManga manga;
|
final MManga manga;
|
||||||
final Source source;
|
final Source source;
|
||||||
|
final TrackSearch? trackSearch;
|
||||||
|
|
||||||
const MigrationMangaGlobalImageCard({
|
const MigrationMangaGlobalImageCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.oldManga,
|
required this.oldManga,
|
||||||
required this.manga,
|
required this.manga,
|
||||||
required this.source,
|
required this.source,
|
||||||
|
this.trackSearch,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -306,7 +322,11 @@ class _MigrationMangaGlobalImageCardState
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) {
|
builder: (ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(l10n.migrate_confirm),
|
title: Text(
|
||||||
|
widget.trackSearch == null
|
||||||
|
? l10n.migrate_confirm
|
||||||
|
: l10n.track_library_add_confirm,
|
||||||
|
),
|
||||||
content: preview.chapters != null
|
content: preview.chapters != null
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: ctx.height(0.5),
|
height: ctx.height(0.5),
|
||||||
|
|
@ -385,133 +405,14 @@ class _MigrationMangaGlobalImageCardState
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) => TextButton(
|
builder: (context, ref, child) => TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
String? historyChapter;
|
if (widget.trackSearch == null) {
|
||||||
String? historyDate;
|
await _migrateManga(preview);
|
||||||
List<Chapter> chaptersProgress = [];
|
if (ctx.mounted) {
|
||||||
isar.writeTxnSync(() {
|
Navigator.pop(ctx);
|
||||||
final histories = isar.historys
|
Navigator.pop(ctx);
|
||||||
.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) {
|
} else {
|
||||||
chaptersProgress.add(chapter);
|
await _addTrackManga(context);
|
||||||
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),
|
child: Text(l10n.ok),
|
||||||
|
|
@ -527,6 +428,243 @@ class _MigrationMangaGlobalImageCardState
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _addTrackManga(BuildContext context) async {
|
||||||
|
List<int> categoryIds = [];
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.set_categories),
|
||||||
|
content: SizedBox(
|
||||||
|
width: context.width(0.8),
|
||||||
|
child: StreamBuilder(
|
||||||
|
stream: isar.categorys
|
||||||
|
.filter()
|
||||||
|
.idIsNotNull()
|
||||||
|
.and()
|
||||||
|
.forItemTypeEqualTo(widget.oldManga.itemType)
|
||||||
|
.watch(fireImmediately: true),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
||||||
|
final entries = snapshot.data!;
|
||||||
|
return SuperListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: entries.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ListTileChapterFilter(
|
||||||
|
label: entries[index].name!,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (categoryIds.contains(entries[index].id)) {
|
||||||
|
categoryIds.remove(entries[index].id);
|
||||||
|
} else {
|
||||||
|
categoryIds.add(entries[index].id!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
type: categoryIds.contains(entries[index].id)
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.push(
|
||||||
|
"/categories",
|
||||||
|
extra: (
|
||||||
|
true,
|
||||||
|
widget.oldManga.itemType == ItemType.manga
|
||||||
|
? 0
|
||||||
|
: widget.oldManga.itemType == ItemType.anime
|
||||||
|
? 1
|
||||||
|
: 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(l10n.edit),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final model = widget.manga;
|
||||||
|
final manga = Manga(
|
||||||
|
name: model.name,
|
||||||
|
artist: model.artist,
|
||||||
|
author: model.author,
|
||||||
|
description: model.description,
|
||||||
|
imageUrl: model.imageUrl,
|
||||||
|
link: model.link,
|
||||||
|
genre: model.genre,
|
||||||
|
status: model.status ?? Status.unknown,
|
||||||
|
source: widget.source.name,
|
||||||
|
lang: widget.source.lang,
|
||||||
|
itemType: widget.oldManga.itemType,
|
||||||
|
favorite: true,
|
||||||
|
categories: categoryIds,
|
||||||
|
dateAdded: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
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<void> _migrateManga(MManga preview) async {
|
||||||
|
String? historyChapter;
|
||||||
|
String? historyDate;
|
||||||
|
List<Chapter> chaptersProgress = [];
|
||||||
|
isar.writeTxnSync(() {
|
||||||
|
final histories = isar.historys
|
||||||
|
.filter()
|
||||||
|
.mangaIdEqualTo(widget.oldManga.id)
|
||||||
|
.sortByDate()
|
||||||
|
.findAllSync();
|
||||||
|
historyChapter = _extractChapterNumber(
|
||||||
|
histories.lastOrNull?.chapter.value?.name ?? "",
|
||||||
|
);
|
||||||
|
historyDate = histories.lastOrNull?.date;
|
||||||
|
for (var history in histories) {
|
||||||
|
isar.historys.deleteSync(history.id!);
|
||||||
|
ref
|
||||||
|
.read(synchingProvider(syncId: 1).notifier)
|
||||||
|
.addChangedPart(ActionType.removeHistory, history.id, "{}", false);
|
||||||
|
}
|
||||||
|
for (var chapter in widget.oldManga.chapters) {
|
||||||
|
chaptersProgress.add(chapter);
|
||||||
|
isar.updates
|
||||||
|
.filter()
|
||||||
|
.mangaIdEqualTo(chapter.mangaId)
|
||||||
|
.chapterNameEqualTo(chapter.name)
|
||||||
|
.deleteAllSync();
|
||||||
|
isar.chapters.deleteSync(chapter.id!);
|
||||||
|
ref
|
||||||
|
.read(synchingProvider(syncId: 1).notifier)
|
||||||
|
.addChangedPart(ActionType.removeChapter, chapter.id, "{}", false);
|
||||||
|
}
|
||||||
|
widget.oldManga.name = widget.manga.name;
|
||||||
|
widget.oldManga.link = widget.manga.link;
|
||||||
|
widget.oldManga.imageUrl = widget.manga.imageUrl;
|
||||||
|
widget.oldManga.lang = widget.source.lang;
|
||||||
|
widget.oldManga.source = widget.source.name;
|
||||||
|
widget.oldManga.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) {
|
String? _extractChapterNumber(String chapterName) {
|
||||||
return RegExp(
|
return RegExp(
|
||||||
r'\s*(\d+\.\d+)\s*',
|
r'\s*(\d+\.\d+)\s*',
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ final navigationItems = {
|
||||||
"/history": "History",
|
"/history": "History",
|
||||||
"/browse": "Browse",
|
"/browse": "Browse",
|
||||||
"/more": "More",
|
"/more": "More",
|
||||||
|
"/trackerLibrary/anilist": "AL",
|
||||||
|
"/trackerLibrary/kitsu": "Kitsu",
|
||||||
|
"/trackerLibrary/mal": "MAL",
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsSection extends StatelessWidget {
|
class SettingsSection extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -149,18 +149,31 @@ class FullScreenReaderState extends _$FullScreenReaderState {
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class NavigationOrderState extends _$NavigationOrderState {
|
class NavigationOrderState extends _$NavigationOrderState {
|
||||||
|
final items = [
|
||||||
|
'/MangaLibrary',
|
||||||
|
'/AnimeLibrary',
|
||||||
|
'/NovelLibrary',
|
||||||
|
'/updates',
|
||||||
|
'/history',
|
||||||
|
'/browse',
|
||||||
|
'/more',
|
||||||
|
'/trackerLibrary/anilist',
|
||||||
|
'/trackerLibrary/kitsu',
|
||||||
|
'/trackerLibrary/mal',
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> build() {
|
List<String> build() {
|
||||||
return isar.settings.getSync(227)!.navigationOrder ??
|
return _checkMissingItems(
|
||||||
[
|
isar.settings.getSync(227)!.navigationOrder?.toList() ?? [],
|
||||||
'/MangaLibrary',
|
);
|
||||||
'/AnimeLibrary',
|
}
|
||||||
'/NovelLibrary',
|
|
||||||
'/updates',
|
List<String> _checkMissingItems(List<String> navigationOrder) {
|
||||||
'/history',
|
navigationOrder.addAll(
|
||||||
'/browse',
|
items.where((e) => !navigationOrder.contains(e)).toList(),
|
||||||
'/more',
|
);
|
||||||
];
|
return navigationOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(List<String> values) {
|
void set(List<String> values) {
|
||||||
|
|
@ -176,7 +189,12 @@ class NavigationOrderState extends _$NavigationOrderState {
|
||||||
class HideItemsState extends _$HideItemsState {
|
class HideItemsState extends _$HideItemsState {
|
||||||
@override
|
@override
|
||||||
List<String> build() {
|
List<String> build() {
|
||||||
return isar.settings.getSync(227)!.hideItems ?? [];
|
return isar.settings.getSync(227)!.hideItems ??
|
||||||
|
[
|
||||||
|
'/trackerLibrary/anilist',
|
||||||
|
'/trackerLibrary/kitsu',
|
||||||
|
'/trackerLibrary/mal',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(List<String> values) {
|
void set(List<String> values) {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ final fullScreenReaderStateProvider =
|
||||||
|
|
||||||
typedef _$FullScreenReaderState = AutoDisposeNotifier<bool>;
|
typedef _$FullScreenReaderState = AutoDisposeNotifier<bool>;
|
||||||
String _$navigationOrderStateHash() =>
|
String _$navigationOrderStateHash() =>
|
||||||
r'f1da55a7687995d136a6580d3f63f9b1b32a6ae8';
|
r'f300869743afaccfd47210115f341d25fec522bb';
|
||||||
|
|
||||||
/// See also [NavigationOrderState].
|
/// See also [NavigationOrderState].
|
||||||
@ProviderFor(NavigationOrderState)
|
@ProviderFor(NavigationOrderState)
|
||||||
|
|
@ -174,7 +174,7 @@ final navigationOrderStateProvider =
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$NavigationOrderState = AutoDisposeNotifier<List<String>>;
|
typedef _$NavigationOrderState = AutoDisposeNotifier<List<String>>;
|
||||||
String _$hideItemsStateHash() => r'b4a467e66f6a1f9b36e4b201a10b771e0dae6a80';
|
String _$hideItemsStateHash() => r'6844a05786f6c547a7cba261f742e82d871b6cb1';
|
||||||
|
|
||||||
/// See also [HideItemsState].
|
/// See also [HideItemsState].
|
||||||
@ProviderFor(HideItemsState)
|
@ProviderFor(HideItemsState)
|
||||||
|
|
|
||||||
387
lib/modules/tracker_library/tracker_library_screen.dart
Normal file
387
lib/modules/tracker_library/tracker_library_screen.dart
Normal file
|
|
@ -0,0 +1,387 @@
|
||||||
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
|
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/main.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/track.dart';
|
||||||
|
import 'package:mangayomi/models/track_search.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/bottom_text_widget.dart';
|
||||||
|
import 'package:mangayomi/providers/l10n_providers.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:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
|
enum TrackerProviders {
|
||||||
|
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, required this.name});
|
||||||
|
|
||||||
|
final int syncId;
|
||||||
|
final String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackLibrarySection {
|
||||||
|
String name;
|
||||||
|
Future<List<TrackSearch>?> Function() func;
|
||||||
|
ItemType itemType;
|
||||||
|
|
||||||
|
TrackLibrarySection({
|
||||||
|
required this.name,
|
||||||
|
required this.func,
|
||||||
|
this.itemType = ItemType.manga,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackerLibraryScreen extends ConsumerStatefulWidget {
|
||||||
|
final TrackerProviders trackerProvider;
|
||||||
|
final String? presetInput;
|
||||||
|
const TrackerLibraryScreen({
|
||||||
|
required this.trackerProvider,
|
||||||
|
required this.presetInput,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<TrackerLibraryScreen> createState() =>
|
||||||
|
_TrackerLibraryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final sections = [
|
||||||
|
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: "Continue watching",
|
||||||
|
func: fetchUserData(ItemType.anime),
|
||||||
|
itemType: ItemType.anime,
|
||||||
|
),
|
||||||
|
TrackLibrarySection(
|
||||||
|
name: "Popular Manga",
|
||||||
|
func: fetchGeneralData(ItemType.manga, rankingType: "bypopularity"),
|
||||||
|
),
|
||||||
|
TrackLibrarySection(
|
||||||
|
name: "Top Manga",
|
||||||
|
func: fetchGeneralData(ItemType.manga, rankingType: "manga"),
|
||||||
|
),
|
||||||
|
TrackLibrarySection(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TrackSearch>?> 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<List<TrackSearch>?> Function() fetchUserData(ItemType itemType) {
|
||||||
|
return () async => await ref
|
||||||
|
.read(
|
||||||
|
trackStateProvider(
|
||||||
|
track: Track(
|
||||||
|
syncId: widget.trackerProvider.syncId,
|
||||||
|
status: TrackStatus.completed,
|
||||||
|
),
|
||||||
|
itemType: itemType,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.fetchUserData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackerSectionScreen extends StatefulWidget {
|
||||||
|
final TrackLibrarySection section;
|
||||||
|
|
||||||
|
const TrackerSectionScreen({super.key, required this.section});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TrackerSectionScreen> createState() => _TrackerSectionScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrackerSectionScreenState extends State<TrackerSectionScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _errorMessage = "";
|
||||||
|
bool _isLoading = true;
|
||||||
|
List<TrackSearch> tracks = [];
|
||||||
|
_init() async {
|
||||||
|
try {
|
||||||
|
_errorMessage = "";
|
||||||
|
tracks = await widget.section.func() ?? [];
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = e.toString();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
height: 260,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(dense: true, title: Text(widget.section.name)),
|
||||||
|
Flexible(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (_errorMessage.isNotEmpty) {
|
||||||
|
return Center(child: Text(_errorMessage));
|
||||||
|
}
|
||||||
|
if (tracks.isNotEmpty) {
|
||||||
|
return SuperListView.builder(
|
||||||
|
extentPrecalculationPolicy:
|
||||||
|
SuperPrecalculationPolicy(),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: tracks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return TrackerLibraryImageCard(
|
||||||
|
track: tracks[index],
|
||||||
|
itemType: widget.section.itemType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Center(child: Text(l10n.no_result));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackerLibraryImageCard extends ConsumerStatefulWidget {
|
||||||
|
final TrackSearch track;
|
||||||
|
final ItemType itemType;
|
||||||
|
|
||||||
|
const TrackerLibraryImageCard({
|
||||||
|
super.key,
|
||||||
|
required this.track,
|
||||||
|
required this.itemType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<TrackerLibraryImageCard> createState() =>
|
||||||
|
_TrackerLibraryImageCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrackerLibraryImageCardState
|
||||||
|
extends ConsumerState<TrackerLibraryImageCard>
|
||||||
|
with AutomaticKeepAliveClientMixin<TrackerLibraryImageCard> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
final trackData = widget.track;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _pushMigrationScreen(context),
|
||||||
|
child: StreamBuilder(
|
||||||
|
stream: isar.mangas
|
||||||
|
.filter()
|
||||||
|
.itemTypeEqualTo(widget.itemType)
|
||||||
|
.nameEqualTo(trackData.title)
|
||||||
|
.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(
|
||||||
|
imageUrl: toImgUrl(
|
||||||
|
hasData
|
||||||
|
? snapshot
|
||||||
|
.data!
|
||||||
|
.first
|
||||||
|
.customCoverFromTracker ??
|
||||||
|
snapshot.data!.first.imageUrl ??
|
||||||
|
""
|
||||||
|
: trackData.coverUrl ?? "",
|
||||||
|
),
|
||||||
|
width: 110,
|
||||||
|
height: 150,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BottomTextWidget(
|
||||||
|
fontSize: 12.0,
|
||||||
|
text: trackData.title!,
|
||||||
|
isLoading: true,
|
||||||
|
textColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||||
|
isComfortableGrid: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 110,
|
||||||
|
height: 150,
|
||||||
|
color: hasData && snapshot.data!.first.favorite!
|
||||||
|
? Colors.black.withValues(alpha: 0.7)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (hasData && snapshot.data!.first.favorite!)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Icon(
|
||||||
|
Icons.collections_bookmark,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Icon(Icons.star, color: context.primaryColor),
|
||||||
|
),
|
||||||
|
TextSpan(text: " ${trackData.score ?? "?"}"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pushMigrationScreen(BuildContext context) {
|
||||||
|
context.push(
|
||||||
|
"/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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SuperPrecalculationPolicy extends ExtentPrecalculationPolicy {
|
||||||
|
@override
|
||||||
|
bool shouldPrecalculateExtents(ExtentPrecalculationContext context) {
|
||||||
|
return context.numberOfItems < 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:mangayomi/models/manga.dart';
|
import 'package:mangayomi/models/manga.dart';
|
||||||
import 'package:mangayomi/models/source.dart';
|
import 'package:mangayomi/models/source.dart';
|
||||||
import 'package:mangayomi/models/track_preference.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/anime/anime_player_view.dart';
|
||||||
import 'package:mangayomi/modules/browse/extension/edit_code.dart';
|
import 'package:mangayomi/modules/browse/extension/edit_code.dart';
|
||||||
import 'package:mangayomi/modules/browse/extension/extension_detail.dart';
|
import 'package:mangayomi/modules/browse/extension/extension_detail.dart';
|
||||||
|
|
@ -18,6 +19,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/settings/reader/providers/reader_state_provider.dart';
|
||||||
import 'package:mangayomi/modules/more/statistics/statistics_screen.dart';
|
import 'package:mangayomi/modules/more/statistics/statistics_screen.dart';
|
||||||
import 'package:mangayomi/modules/novel/novel_reader_view.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/updates/updates_screen.dart';
|
||||||
import 'package:mangayomi/modules/more/categories/categories_screen.dart';
|
import 'package:mangayomi/modules/more/categories/categories_screen.dart';
|
||||||
import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart';
|
import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart';
|
||||||
|
|
@ -122,6 +124,27 @@ class RouterNotifier extends ChangeNotifier {
|
||||||
builder: (id) =>
|
builder: (id) =>
|
||||||
LibraryScreen(itemType: ItemType.novel, presetInput: id),
|
LibraryScreen(itemType: ItemType.novel, presetInput: id),
|
||||||
),
|
),
|
||||||
|
_genericRoute<String?>(
|
||||||
|
name: "trackerLibrary/anilist",
|
||||||
|
builder: (id) => TrackerLibraryScreen(
|
||||||
|
trackerProvider: TrackerProviders.anilist,
|
||||||
|
presetInput: id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_genericRoute<String?>(
|
||||||
|
name: "trackerLibrary/kitsu",
|
||||||
|
builder: (id) => TrackerLibraryScreen(
|
||||||
|
trackerProvider: TrackerProviders.kitsu,
|
||||||
|
presetInput: id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_genericRoute<String?>(
|
||||||
|
name: "trackerLibrary/mal",
|
||||||
|
builder: (id) => TrackerLibraryScreen(
|
||||||
|
trackerProvider: TrackerProviders.myAnimeList,
|
||||||
|
presetInput: id,
|
||||||
|
),
|
||||||
|
),
|
||||||
_genericRoute(name: "history", child: const HistoryScreen()),
|
_genericRoute(name: "history", child: const HistoryScreen()),
|
||||||
_genericRoute(name: "updates", child: const UpdatesScreen()),
|
_genericRoute(name: "updates", child: const UpdatesScreen()),
|
||||||
_genericRoute(name: "browse", child: const BrowseScreen()),
|
_genericRoute(name: "browse", child: const BrowseScreen()),
|
||||||
|
|
@ -208,6 +231,10 @@ class RouterNotifier extends ChangeNotifier {
|
||||||
name: "migrate",
|
name: "migrate",
|
||||||
builder: (manga) => MigrationScreen(manga: manga),
|
builder: (manga) => MigrationScreen(manga: manga),
|
||||||
),
|
),
|
||||||
|
_genericRoute<(Manga, TrackSearch)>(
|
||||||
|
name: "migrate/tracker",
|
||||||
|
builder: (data) => MigrationScreen(manga: data.$1, trackSearch: data.$2),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
GoRoute _genericRoute<T>({
|
GoRoute _genericRoute<T>({
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,112 @@ class MyAnimeList extends _$MyAnimeList {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<TrackSearch>> fetchGeneralData({
|
||||||
|
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': rankingType,
|
||||||
|
'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<String, dynamic>;
|
||||||
|
|
||||||
|
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"]}",
|
||||||
|
syncId: syncId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TrackSearch>> 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<String, dynamic>;
|
||||||
|
|
||||||
|
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"] is double
|
||||||
|
? e["node"]["mean"]
|
||||||
|
: (e["node"]["mean"] as int).toDouble(),
|
||||||
|
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: _parseDate(
|
||||||
|
e["list_status"]["start_date"],
|
||||||
|
),
|
||||||
|
finishedReadingDate: _parseDate(
|
||||||
|
e["list_status"]["finish_date"],
|
||||||
|
),
|
||||||
|
lastChapterRead:
|
||||||
|
e["list_status"][isManga
|
||||||
|
? "num_chapters_read"
|
||||||
|
: "num_episodes_watched"],
|
||||||
|
status: fromMyAnimeListStatus(
|
||||||
|
e["list_status"]["status"],
|
||||||
|
isManga,
|
||||||
|
).name,
|
||||||
|
syncId: syncId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
String _convertToIsoDate(int? epochTime) {
|
String _convertToIsoDate(int? epochTime) {
|
||||||
String date = "";
|
String date = "";
|
||||||
try {
|
try {
|
||||||
|
|
@ -207,6 +313,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<dynamic> _getOAuth(String code) async {
|
Future<dynamic> _getOAuth(String code) async {
|
||||||
final params = {
|
final params = {
|
||||||
'client_id': clientId,
|
'client_id': clientId,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'myanimelist.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$myAnimeListHash() => r'a7d644ee61119350613a9cff2fbe87dbd2f98912';
|
String _$myAnimeListHash() => r'eb483b6451d34e595bb770eefc0f673df13275b3';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
315
lib/services/trackers/trakt.dart
Normal file
315
lib/services/trackers/trakt.dart
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
/*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<bool?> 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<String, dynamic>)
|
||||||
|
..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<String> _getAccessToken() async {
|
||||||
|
final track = ref.watch(tracksProvider(syncId: syncId));
|
||||||
|
final mALOAuth = OAuth.fromJson(
|
||||||
|
jsonDecode(track!.oAuth!) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
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<String, dynamic>,
|
||||||
|
);
|
||||||
|
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<List<TrackSearch>> 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<String, dynamic>;
|
||||||
|
|
||||||
|
List<int> 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<TrackSearch> 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<String, dynamic>;
|
||||||
|
|
||||||
|
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<int>.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<TrackStatus> 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<dynamic> _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<String> _getUserName(String accessToken) async {
|
||||||
|
final response = await _makeGetRequest(
|
||||||
|
Uri.parse('$baseApiUrl/users/@me'),
|
||||||
|
accessToken,
|
||||||
|
);
|
||||||
|
return jsonDecode(response.body)['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Track> 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<String, dynamic> 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<Track> 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<Response> _makeGetRequest(Uri url, String accessToken) async {
|
||||||
|
return await http.get(
|
||||||
|
url,
|
||||||
|
headers: {'Authorization': 'Bearer $accessToken'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
Loading…
Reference in a new issue