mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +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",
|
||||
"you_have_finished_reading": "You have finished reading",
|
||||
"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:
|
||||
/// **'Hardware Decoder'**
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1517,4 +1517,10 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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
|
||||
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`).
|
||||
|
|
|
|||
|
|
@ -1539,4 +1539,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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
|
||||
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`).
|
||||
|
|
|
|||
|
|
@ -1533,4 +1533,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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
|
||||
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
|
||||
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
|
||||
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: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<MyApp> {
|
|||
routerDelegate: router.routerDelegate,
|
||||
routeInformationProvider: router.routeInformationProvider,
|
||||
title: 'MangaYomi',
|
||||
scrollBehavior: AllowDesktopScrollBehavior(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -230,3 +232,11 @@ class _MyAppState extends ConsumerState<MyApp> {
|
|||
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();
|
||||
_desktopDestinationsCache[cacheKey] = result;
|
||||
|
|
@ -412,6 +448,31 @@ class _MainScreenState extends ConsumerState<MainScreen> {
|
|||
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;
|
||||
|
|
@ -531,6 +592,9 @@ class _TabletLayout extends StatelessWidget {
|
|||
'/updates',
|
||||
'/browse',
|
||||
'/more',
|
||||
'/trackerLibrary/anilist',
|
||||
'/trackerLibrary/kitsu',
|
||||
'/trackerLibrary/mal',
|
||||
};
|
||||
|
||||
return (location == null || validLocations.contains(location)) ? 100 : 0;
|
||||
|
|
@ -598,6 +662,9 @@ class _MobileBottomNavigation extends StatelessWidget {
|
|||
'/updates',
|
||||
'/browse',
|
||||
'/more',
|
||||
'/trackerLibrary/anilist',
|
||||
'/trackerLibrary/kitsu',
|
||||
'/trackerLibrary/mal',
|
||||
};
|
||||
|
||||
return (location == null || validLocations.contains(location)) ? null : 0;
|
||||
|
|
|
|||
|
|
@ -138,4 +138,21 @@ class TrackState extends _$TrackState {
|
|||
final tracker = getNotifier(syncId);
|
||||
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
|
||||
// **************************************************************************
|
||||
|
||||
String _$trackStateHash() => r'4d31a8a939412cabd800f9747bff7a1ac0ef1996';
|
||||
String _$trackStateHash() => r'b10c02c2e50eb1f044a76560093a8dcf232487c5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -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<MigrationScreen> createState() => _MigrationScreenScreenState();
|
||||
|
|
@ -48,7 +54,11 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
|||
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<MigrationScreen> {
|
|||
query: widget.manga.name ?? widget.manga.author ?? "",
|
||||
manga: widget.manga,
|
||||
source: source,
|
||||
trackSearch: widget.trackSearch,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -73,6 +84,7 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
|||
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<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,
|
||||
);
|
||||
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<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) {
|
||||
return RegExp(
|
||||
r'\s*(\d+\.\d+)\s*',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<String> build() {
|
||||
return isar.settings.getSync(227)!.navigationOrder ??
|
||||
[
|
||||
'/MangaLibrary',
|
||||
'/AnimeLibrary',
|
||||
'/NovelLibrary',
|
||||
'/updates',
|
||||
'/history',
|
||||
'/browse',
|
||||
'/more',
|
||||
];
|
||||
return _checkMissingItems(
|
||||
isar.settings.getSync(227)!.navigationOrder?.toList() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _checkMissingItems(List<String> navigationOrder) {
|
||||
navigationOrder.addAll(
|
||||
items.where((e) => !navigationOrder.contains(e)).toList(),
|
||||
);
|
||||
return navigationOrder;
|
||||
}
|
||||
|
||||
void set(List<String> values) {
|
||||
|
|
@ -176,7 +189,12 @@ class NavigationOrderState extends _$NavigationOrderState {
|
|||
class HideItemsState extends _$HideItemsState {
|
||||
@override
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ final fullScreenReaderStateProvider =
|
|||
|
||||
typedef _$FullScreenReaderState = AutoDisposeNotifier<bool>;
|
||||
String _$navigationOrderStateHash() =>
|
||||
r'f1da55a7687995d136a6580d3f63f9b1b32a6ae8';
|
||||
r'f300869743afaccfd47210115f341d25fec522bb';
|
||||
|
||||
/// See also [NavigationOrderState].
|
||||
@ProviderFor(NavigationOrderState)
|
||||
|
|
@ -174,7 +174,7 @@ final navigationOrderStateProvider =
|
|||
);
|
||||
|
||||
typedef _$NavigationOrderState = AutoDisposeNotifier<List<String>>;
|
||||
String _$hideItemsStateHash() => r'b4a467e66f6a1f9b36e4b201a10b771e0dae6a80';
|
||||
String _$hideItemsStateHash() => r'6844a05786f6c547a7cba261f742e82d871b6cb1';
|
||||
|
||||
/// See also [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/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';
|
||||
|
|
@ -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/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';
|
||||
|
|
@ -122,6 +124,27 @@ class RouterNotifier extends ChangeNotifier {
|
|||
builder: (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: "updates", child: const UpdatesScreen()),
|
||||
_genericRoute(name: "browse", child: const BrowseScreen()),
|
||||
|
|
@ -208,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<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 date = "";
|
||||
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 {
|
||||
final params = {
|
||||
'client_id': clientId,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'myanimelist.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$myAnimeListHash() => r'a7d644ee61119350613a9cff2fbe87dbd2f98912';
|
||||
String _$myAnimeListHash() => r'eb483b6451d34e595bb770eefc0f673df13275b3';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
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