added Kitsu and Anilist

This commit is contained in:
Schnitzel5 2025-06-15 21:08:34 +02:00
parent 7c8faf8bed
commit 06ca441644
24 changed files with 363 additions and 92 deletions

View file

@ -476,5 +476,6 @@
"return_to_the_list_of_chapters": "Return to the list of chapters",
"hwdec": "Hardware Decoder",
"track_library_add": "Add to local library",
"track_library_add_confirm": "Add tracked item to local library"
"track_library_add_confirm": "Add tracked item to local library",
"track_library_not_logged": "Login to the corresponding tracker to use this feature!"
}

View file

@ -2981,6 +2981,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Add tracked item to local library'**
String get track_library_add_confirm;
/// No description provided for @track_library_not_logged.
///
/// In en, this message translates to:
/// **'Login to the corresponding tracker to use this feature!'**
String get track_library_not_logged;
}
class _AppLocalizationsDelegate

View file

@ -1523,4 +1523,8 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1534,4 +1534,8 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1523,4 +1523,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1540,6 +1540,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).

View file

@ -1545,4 +1545,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1529,4 +1529,8 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1539,4 +1539,8 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1537,6 +1537,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}
/// The translations for Portuguese, as used in Brazil (`pt_BR`).

View file

@ -1539,4 +1539,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1524,4 +1524,8 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1530,4 +1530,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -1492,4 +1492,8 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get track_library_add_confirm => 'Add tracked item to local library';
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
}

View file

@ -139,15 +139,15 @@ class TrackState extends _$TrackState {
return await tracker.search(query, _isManga);
}
Future<List<TrackSearch>?> fetchGeneralData({
String rankingType = "airing",
}) async {
Future<List<TrackSearch>?> fetchGeneralData({String? rankingType}) async {
final syncId = track!.syncId!;
final tracker = getNotifier(syncId);
return await tracker.fetchGeneralData(
isManga: _isManga,
rankingType: rankingType,
);
return rankingType != null
? await tracker.fetchGeneralData(
isManga: _isManga,
rankingType: rankingType,
)
: await tracker.fetchGeneralData(isManga: _isManga);
}
Future<List<TrackSearch>?> fetchUserData() async {

View file

@ -6,7 +6,7 @@ part of 'track_state_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$trackStateHash() => r'b10c02c2e50eb1f044a76560093a8dcf232487c5';
String _$trackStateHash() => r'b70770f8524a0d9059ffd3f52b42634c16672a0f';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -175,7 +175,7 @@ final onlyIncludePinnedSourceStateProvider =
typedef _$OnlyIncludePinnedSourceState = AutoDisposeNotifier<bool>;
String _$extensionsRepoStateHash() =>
r'9e59b257433ed7f999dd4800f6ecb8c13c8b2c6a';
r'3ea91c79a9bc24f086fce49de2c61a1e06bad1fe';
abstract class _$ExtensionsRepoState
extends BuildlessAutoDisposeNotifier<List<Repo>> {

View file

@ -3,11 +3,11 @@
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_preference.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/tracker_library/tracker_item_card.dart';
@ -59,6 +59,7 @@ class TrackerLibraryScreen extends ConsumerStatefulWidget {
class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context)!;
final sections = switch (widget.trackerProvider.syncId) {
1 => _sectionsMAL(),
2 => _sectionsAL(),
@ -70,15 +71,36 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
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),
);
child: StreamBuilder(
stream: isar.trackPreferences
.filter()
.syncIdEqualTo(widget.trackerProvider.syncId)
.watch(fireImmediately: true),
builder: (context, snapshot) {
List<TrackPreference> entries = snapshot.hasData
? snapshot.data ?? []
: [];
return entries.isNotEmpty
? SuperListView.builder(
itemCount: sections.length,
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
itemBuilder: (context, index) {
final section = sections[index];
return SizedBox(
height: 260,
child: TrackerSectionScreen(section: section),
);
},
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text(l10n.track_library_not_logged)],
),
],
);
},
),
),
@ -133,18 +155,18 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
List<TrackLibrarySection> _sectionsKitsu() {
return [
TrackLibrarySection(
name: "Airing Anime",
name: "Popular Anime",
func: _fetchGeneralData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "bypopularity"),
name: "Latest Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "-updatedAt"),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Upcoming Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "upcoming"),
name: "Best Rated Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "-averageRating"),
itemType: ItemType.anime,
),
TrackLibrarySection(
@ -154,11 +176,15 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
),
TrackLibrarySection(
name: "Popular Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "bypopularity"),
func: _fetchGeneralData(ItemType.manga),
),
TrackLibrarySection(
name: "Top Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "manga"),
name: "Latest Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "-updatedAt"),
),
TrackLibrarySection(
name: "Best Rated Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "-averageRating"),
),
TrackLibrarySection(
name: "Continue reading",
@ -170,18 +196,33 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
List<TrackLibrarySection> _sectionsAL() {
return [
TrackLibrarySection(
name: "Airing Anime",
name: "Upcoming Anime",
func: _fetchGeneralData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "bypopularity"),
func: _fetchGeneralData(
ItemType.anime,
rankingType: "sort: POPULARITY_DESC",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Upcoming Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "upcoming"),
name: "Trending Anime",
func: _fetchGeneralData(
ItemType.anime,
rankingType: "sort: TRENDING_DESC",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Latest Anime",
func: _fetchGeneralData(
ItemType.anime,
rankingType:
"sort: [UPDATED_AT_DESC, POPULARITY_DESC], status: RELEASING",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
@ -189,21 +230,31 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
func: _fetchUserData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Upcoming Manga",
func: _fetchGeneralData(ItemType.manga),
),
TrackLibrarySection(
name: "Popular Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "bypopularity"),
func: _fetchGeneralData(
ItemType.manga,
rankingType: "sort: POPULARITY_DESC",
),
),
TrackLibrarySection(
name: "Top Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "manga"),
name: "Trending Manga",
func: _fetchGeneralData(
ItemType.manga,
rankingType: "sort: TRENDING_DESC",
),
),
TrackLibrarySection(
name: "Top Manhwa",
func: _fetchGeneralData(ItemType.manga, rankingType: "manhwa"),
),
TrackLibrarySection(
name: "Top Manhua",
func: _fetchGeneralData(ItemType.manga, rankingType: "manhua"),
name: "Latest Manga",
func: _fetchGeneralData(
ItemType.manga,
rankingType:
"sort: [UPDATED_AT_DESC, POPULARITY_DESC], status: RELEASING",
),
),
TrackLibrarySection(
name: "Continue reading",
@ -214,7 +265,7 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
Future<List<TrackSearch>?> Function() _fetchGeneralData(
ItemType itemType, {
String rankingType = "airing",
String? rankingType,
}) {
return () async => await ref
.read(
@ -279,6 +330,7 @@ class _TrackerSectionScreenState extends State<TrackerSectionScreen> {
_isLoading = false;
});
}
rethrow;
}
}

View file

@ -221,6 +221,136 @@ class Anilist extends _$Anilist {
..totalChapter = jsonRes['media'][contentUnit] as int? ?? 0;
}
Future<List<TrackSearch>> fetchGeneralData({
bool isManga = true,
String rankingType =
"status: NOT_YET_RELEASED, sort: [POPULARITY_DESC, TRENDING_DESC]",
}) async {
final type = isManga ? "MANGA" : "ANIME";
final contentUnit = isManga ? "chapters" : "episodes";
final query =
'''
query {
Page(perPage: 50) {
media(type: $type, format_not_in: [NOVEL], $rankingType) {
id
title { userPreferred }
coverImage { large }
format
status
$contentUnit
description
startDate { year month day }
averageScore
}
}
}
''';
final Map<String, dynamic> vars = {};
final data = await _executeGraphQL(query, vars);
final entries = List<Map<String, dynamic>>.from(
data['Page']['media'] as List,
);
return entries
.map(
(jsonRes) => TrackSearch(
libraryId: jsonRes['id'],
syncId: syncId,
trackingUrl: "",
mediaId: jsonRes['id'],
summary: jsonRes['description'] ?? "",
totalChapter: jsonRes[contentUnit] ?? 0,
coverUrl: jsonRes['coverImage']['large'] ?? "",
title: jsonRes['title']['userPreferred'],
startDate:
jsonRes["start_date"] ??
DateTime.fromMillisecondsSinceEpoch(
parseDate(jsonRes, 'startDate'),
).toString(),
publishingType: "",
publishingStatus: jsonRes['status'],
score: jsonRes["averageScore"] != null
? jsonRes["averageScore"] * 1.0
: 0,
),
)
.toList();
}
Future<List<TrackSearch>> fetchUserData({bool isManga = true}) async {
final userId = int.parse(
ref.watch(tracksProvider(syncId: syncId))!.username!,
);
final type = isManga ? "MANGA" : "ANIME";
final contentUnit = isManga ? "chapters" : "episodes";
final query =
'''
query(\$id: Int!) {
Page {
mediaList(userId: \$id, type: $type) {
id
status
scoreRaw: score(format: POINT_100)
progress
startedAt { year month day }
completedAt { year month day }
media {
id
title { userPreferred }
coverImage { large }
format
status
$contentUnit
description
startDate { year month day }
averageScore
}
}
}
}
''';
final vars = {"id": userId};
final data = await _executeGraphQL(query, vars);
final entries = List<Map<String, dynamic>>.from(
data['Page']['mediaList'] as List,
);
return entries
.map(
(jsonRes) => TrackSearch(
libraryId: jsonRes['id'],
syncId: syncId,
trackingUrl: "",
mediaId: jsonRes['media']['id'],
summary: jsonRes['media']['description'] ?? "",
totalChapter: jsonRes['media'][contentUnit] ?? 0,
coverUrl: jsonRes['media']['coverImage']['large'] ?? "",
title: jsonRes['media']['title']['userPreferred'],
startDate:
jsonRes['media']["start_date"] ??
DateTime.fromMillisecondsSinceEpoch(
parseDate(jsonRes['media'], 'startDate'),
).toString(),
publishingType: "",
publishingStatus: jsonRes['media']['status'],
score: jsonRes['media']['averageScore'] != null
? jsonRes['media']['averageScore'] * 1.0
: 0,
status: _getALTrackStatus(jsonRes['status'], isManga).name,
lastChapterRead: jsonRes['progress'] as int? ?? 0,
startedReadingDate: parseDate(jsonRes, 'startedAt'),
finishedReadingDate: parseDate(jsonRes, 'completedAt'),
),
)
.toList();
}
Future<Map<String, dynamic>> _executeGraphQL(
String document,
Map<String, dynamic> variables,

View file

@ -6,7 +6,7 @@ part of 'anilist.dart';
// RiverpodGenerator
// **************************************************************************
String _$anilistHash() => r'd672e47052f0b40088dd477b7918dc1e06654b48';
String _$anilistHash() => r'6f84817b91a4d9cbea7beacbaf1a77cbdeac9290';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -174,63 +174,97 @@ class Kitsu extends _$Kitsu {
Future<List<TrackSearch>> fetchGeneralData({
bool isManga = true,
String rankingType = "airing", // /anime?sort=-updatedAt
String rankingType = "popularityRank",
}) async {
final response = await http.get(Uri.parse("$_baseUrl/trending/anime"));
final response = await http.get(
Uri.parse("$_baseUrl${isManga ? "manga" : "anime"}?sort=$rankingType"),
);
final data = json.decode(response.body);
final entries = List<Map<String, dynamic>>.from(
data['hits'],
data['data'],
).where((element) => element["subtype"] != "novel").toList();
final totalChapter = isManga ? "chapterCount" : "episodeCount";
return entries
.map(
(jsonRes) => TrackSearch(
libraryId: jsonRes['id'],
syncId: syncId,
trackingUrl: _mediaUrl(isManga ? 'manga' : 'anime', jsonRes['id']),
mediaId: jsonRes['id'],
summary: jsonRes['synopsis'] ?? "",
totalChapter: (jsonRes[totalChapter] ?? 0),
coverUrl: jsonRes['posterImage']['original'] ?? "",
title: jsonRes['canonicalTitle'],
startDate: "",
publishingType: (jsonRes["subtype"] ?? ""),
publishingStatus: jsonRes['endDate'] == null
? "Publishing"
: "Finished",
),
)
.toList();
return entries.map((jsonRes) {
final mediaId = jsonRes['id'] is String
? int.parse(jsonRes['id'])
: jsonRes['id'];
final score = jsonRes['attributes']['averageRating'] is String
? double.parse(jsonRes['attributes']['averageRating'])
: jsonRes['attributes']['averageRating'];
return TrackSearch(
libraryId: mediaId,
syncId: syncId,
trackingUrl: _mediaUrl(isManga ? 'manga' : 'anime', mediaId),
mediaId: mediaId,
summary: jsonRes['attributes']['synopsis'] ?? "",
totalChapter: (jsonRes['attributes'][totalChapter] ?? 0),
coverUrl: jsonRes['attributes']['posterImage']['original'] ?? "",
title: jsonRes['attributes']['canonicalTitle'],
startDate: "",
score: score,
publishingType: (jsonRes['attributes']['subtype'] ?? ""),
publishingStatus: jsonRes['attributes']['endDate'] == null
? "Publishing"
: "Finished",
);
}).toList();
}
Future<List<TrackSearch>> fetchUserData({bool isManga = true}) async {
final response = await http.get(Uri.parse("$_baseUrl/trending/anime"));
final type = isManga ? "manga" : "anime";
final userId = _getUserId();
final accessToken = _getAccessToken();
final response = await _makeGetRequest(
Uri.parse("${_baseUrl}library-entries").replace(
queryParameters: {
'filter[user_id]': userId,
'filter[kind]': type,
'page[limit]': "100",
'sort': "status,-progressed_at",
'include': type,
},
),
accessToken,
);
final data = json.decode(response.body);
final entries = List<Map<String, dynamic>>.from(
data['hits'],
).where((element) => element["subtype"] != "novel").toList();
final totalChapter = isManga ? "chapterCount" : "episodeCount";
return entries
.map(
(jsonRes) => TrackSearch(
libraryId: jsonRes['id'],
syncId: syncId,
trackingUrl: _mediaUrl(isManga ? 'manga' : 'anime', jsonRes['id']),
mediaId: jsonRes['id'],
summary: jsonRes['synopsis'] ?? "",
totalChapter: (jsonRes[totalChapter] ?? 0),
coverUrl: jsonRes['posterImage']['original'] ?? "",
title: jsonRes['canonicalTitle'],
startDate: "",
publishingType: (jsonRes["subtype"] ?? ""),
publishingStatus: jsonRes['endDate'] == null
? "Publishing"
: "Finished",
),
)
.toList();
final totalChapter = type == 'manga' ? "chapterCount" : "episodeCount";
final List<TrackSearch> result = [];
final List<dynamic> dataList = data['data'];
final List<dynamic> includedList = data['included'];
for (int i = 0; i < dataList.length; i++) {
final obj = dataList[i];
final attributes = obj["attributes"];
final included = includedList[i]["attributes"];
final id = int.parse(obj["id"]);
result.add(
TrackSearch(
libraryId: id,
mediaId: id,
syncId: syncId,
trackingUrl: _mediaUrl(type, id),
summary: included['synopsis'] ?? "",
totalChapter: included[totalChapter] ?? 0,
coverUrl: included['posterImage']['original'] ?? "",
title: included['canonicalTitle'],
startDate: "",
publishingType: (included["subtype"] ?? ""),
publishingStatus: included['endDate'] == null
? "Publishing"
: "Finished",
score: included['averageRating'] is String
? double.parse(included['averageRating'])
: included['averageRating'],
status: getKitsuTrackStatus(attributes["status"], type).name,
lastChapterRead: attributes["progress"],
startedReadingDate: _parseDate(attributes["startedAt"]),
finishedReadingDate: _parseDate(attributes["finishedAt"]),
),
);
}
return result;
}
Future<Track?> findLibItem(Track track, bool isManga) async {

View file

@ -6,7 +6,7 @@ part of 'kitsu.dart';
// RiverpodGenerator
// **************************************************************************
String _$kitsuHash() => r'59e556e6382198c1cbef094491d6329861ff2e75';
String _$kitsuHash() => r'bcedbbc9460d79b4a026210261f63f77ec9958fd';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -152,7 +152,7 @@ class MyAnimeList extends _$MyAnimeList {
Future<List<TrackSearch>> fetchGeneralData({
bool isManga = true,
String rankingType =
"airing", // bypopularity, tv, upcoming - all, manga, manhwa, manhua
"airing",
}) async {
final accessToken = await _getAccessToken();
final item = isManga ? "manga" : "anime";

View file

@ -6,7 +6,7 @@ part of 'myanimelist.dart';
// RiverpodGenerator
// **************************************************************************
String _$myAnimeListHash() => r'eb483b6451d34e595bb770eefc0f673df13275b3';
String _$myAnimeListHash() => r'5e1144313509b9556bd0da4246f9fb22d00722a5';
/// Copied from Dart SDK
class _SystemHash {