diff --git a/assets/tracker_anilist.webp b/assets/tracker_anilist.webp new file mode 100644 index 0000000..b553d6c Binary files /dev/null and b/assets/tracker_anilist.webp differ diff --git a/lib/models/track.dart b/lib/models/track.dart index f6989cb..9f4b8b2 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -6,6 +6,8 @@ part 'track.g.dart'; class Track { Id? id; + int? libraryId; + int? mediaId; int? mangaId; @@ -14,7 +16,7 @@ class Track { String? title; - int ?lastChapterRead; + int? lastChapterRead; int? totalChapter; @@ -31,6 +33,7 @@ class Track { Track( {this.id = Isar.autoIncrement, + this.libraryId, this.mediaId, this.mangaId, this.syncId, diff --git a/lib/models/track.g.dart b/lib/models/track.g.dart index b31d550..352b8cd 100644 --- a/lib/models/track.g.dart +++ b/lib/models/track.g.dart @@ -27,49 +27,54 @@ const TrackSchema = CollectionSchema( name: r'lastChapterRead', type: IsarType.long, ), - r'mangaId': PropertySchema( + r'libraryId': PropertySchema( id: 2, + name: r'libraryId', + type: IsarType.long, + ), + r'mangaId': PropertySchema( + id: 3, name: r'mangaId', type: IsarType.long, ), r'mediaId': PropertySchema( - id: 3, + id: 4, name: r'mediaId', type: IsarType.long, ), r'score': PropertySchema( - id: 4, + id: 5, name: r'score', type: IsarType.long, ), r'startedReadingDate': PropertySchema( - id: 5, + id: 6, name: r'startedReadingDate', type: IsarType.long, ), r'status': PropertySchema( - id: 6, + id: 7, name: r'status', type: IsarType.byte, enumMap: _TrackstatusEnumValueMap, ), r'syncId': PropertySchema( - id: 7, + id: 8, name: r'syncId', type: IsarType.long, ), r'title': PropertySchema( - id: 8, + id: 9, name: r'title', type: IsarType.string, ), r'totalChapter': PropertySchema( - id: 9, + id: 10, name: r'totalChapter', type: IsarType.long, ), r'trackingUrl': PropertySchema( - id: 10, + id: 11, name: r'trackingUrl', type: IsarType.string, ) @@ -117,15 +122,16 @@ void _trackSerialize( ) { writer.writeLong(offsets[0], object.finishedReadingDate); writer.writeLong(offsets[1], object.lastChapterRead); - writer.writeLong(offsets[2], object.mangaId); - writer.writeLong(offsets[3], object.mediaId); - writer.writeLong(offsets[4], object.score); - writer.writeLong(offsets[5], object.startedReadingDate); - writer.writeByte(offsets[6], object.status.index); - writer.writeLong(offsets[7], object.syncId); - writer.writeString(offsets[8], object.title); - writer.writeLong(offsets[9], object.totalChapter); - writer.writeString(offsets[10], object.trackingUrl); + writer.writeLong(offsets[2], object.libraryId); + writer.writeLong(offsets[3], object.mangaId); + writer.writeLong(offsets[4], object.mediaId); + writer.writeLong(offsets[5], object.score); + writer.writeLong(offsets[6], object.startedReadingDate); + writer.writeByte(offsets[7], object.status.index); + writer.writeLong(offsets[8], object.syncId); + writer.writeString(offsets[9], object.title); + writer.writeLong(offsets[10], object.totalChapter); + writer.writeString(offsets[11], object.trackingUrl); } Track _trackDeserialize( @@ -138,16 +144,17 @@ Track _trackDeserialize( finishedReadingDate: reader.readLongOrNull(offsets[0]), id: id, lastChapterRead: reader.readLongOrNull(offsets[1]), - mangaId: reader.readLongOrNull(offsets[2]), - mediaId: reader.readLongOrNull(offsets[3]), - score: reader.readLongOrNull(offsets[4]), - startedReadingDate: reader.readLongOrNull(offsets[5]), - status: _TrackstatusValueEnumMap[reader.readByteOrNull(offsets[6])] ?? + libraryId: reader.readLongOrNull(offsets[2]), + mangaId: reader.readLongOrNull(offsets[3]), + mediaId: reader.readLongOrNull(offsets[4]), + score: reader.readLongOrNull(offsets[5]), + startedReadingDate: reader.readLongOrNull(offsets[6]), + status: _TrackstatusValueEnumMap[reader.readByteOrNull(offsets[7])] ?? TrackStatus.reading, - syncId: reader.readLongOrNull(offsets[7]), - title: reader.readStringOrNull(offsets[8]), - totalChapter: reader.readLongOrNull(offsets[9]), - trackingUrl: reader.readStringOrNull(offsets[10]), + syncId: reader.readLongOrNull(offsets[8]), + title: reader.readStringOrNull(offsets[9]), + totalChapter: reader.readLongOrNull(offsets[10]), + trackingUrl: reader.readStringOrNull(offsets[11]), ); return object; } @@ -172,15 +179,17 @@ P _trackDeserializeProp

( case 5: return (reader.readLongOrNull(offset)) as P; case 6: + return (reader.readLongOrNull(offset)) as P; + case 7: return (_TrackstatusValueEnumMap[reader.readByteOrNull(offset)] ?? TrackStatus.reading) as P; - case 7: - return (reader.readLongOrNull(offset)) as P; case 8: - return (reader.readStringOrNull(offset)) as P; - case 9: return (reader.readLongOrNull(offset)) as P; + case 9: + return (reader.readStringOrNull(offset)) as P; case 10: + return (reader.readLongOrNull(offset)) as P; + case 11: return (reader.readStringOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -501,6 +510,75 @@ extension TrackQueryFilter on QueryBuilder { }); } + QueryBuilder libraryIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'libraryId', + )); + }); + } + + QueryBuilder libraryIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'libraryId', + )); + }); + } + + QueryBuilder libraryIdEqualTo( + int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'libraryId', + value: value, + )); + }); + } + + QueryBuilder libraryIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'libraryId', + value: value, + )); + }); + } + + QueryBuilder libraryIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'libraryId', + value: value, + )); + }); + } + + QueryBuilder libraryIdBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'libraryId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder mangaIdIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -1286,6 +1364,18 @@ extension TrackQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByLibraryId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'libraryId', Sort.asc); + }); + } + + QueryBuilder sortByLibraryIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'libraryId', Sort.desc); + }); + } + QueryBuilder sortByMangaId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mangaId', Sort.asc); @@ -1432,6 +1522,18 @@ extension TrackQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByLibraryId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'libraryId', Sort.asc); + }); + } + + QueryBuilder thenByLibraryIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'libraryId', Sort.desc); + }); + } + QueryBuilder thenByMangaId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mangaId', Sort.asc); @@ -1554,6 +1656,12 @@ extension TrackQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByLibraryId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'libraryId'); + }); + } + QueryBuilder distinctByMangaId() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'mangaId'); @@ -1630,6 +1738,12 @@ extension TrackQueryProperty on QueryBuilder { }); } + QueryBuilder libraryIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'libraryId'); + }); + } + QueryBuilder mangaIdProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'mangaId'); diff --git a/lib/models/track_preference.dart b/lib/models/track_preference.dart index 56f8275..b43e974 100644 --- a/lib/models/track_preference.dart +++ b/lib/models/track_preference.dart @@ -10,9 +10,12 @@ class TrackPreference { String? oAuth; + String? prefs; + TrackPreference({ this.syncId, this.username, this.oAuth, + this.prefs, }); } diff --git a/lib/models/track_preference.g.dart b/lib/models/track_preference.g.dart index d7bc82c..4262e33 100644 --- a/lib/models/track_preference.g.dart +++ b/lib/models/track_preference.g.dart @@ -22,8 +22,13 @@ const TrackPreferenceSchema = CollectionSchema( name: r'oAuth', type: IsarType.string, ), - r'username': PropertySchema( + r'prefs': PropertySchema( id: 1, + name: r'prefs', + type: IsarType.string, + ), + r'username': PropertySchema( + id: 2, name: r'username', type: IsarType.string, ) @@ -54,6 +59,12 @@ int _trackPreferenceEstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.prefs; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.username; if (value != null) { @@ -70,7 +81,8 @@ void _trackPreferenceSerialize( Map> allOffsets, ) { writer.writeString(offsets[0], object.oAuth); - writer.writeString(offsets[1], object.username); + writer.writeString(offsets[1], object.prefs); + writer.writeString(offsets[2], object.username); } TrackPreference _trackPreferenceDeserialize( @@ -81,8 +93,9 @@ TrackPreference _trackPreferenceDeserialize( ) { final object = TrackPreference( oAuth: reader.readStringOrNull(offsets[0]), + prefs: reader.readStringOrNull(offsets[1]), syncId: id, - username: reader.readStringOrNull(offsets[1]), + username: reader.readStringOrNull(offsets[2]), ); return object; } @@ -98,6 +111,8 @@ P _trackPreferenceDeserializeProp

( return (reader.readStringOrNull(offset)) as P; case 1: return (reader.readStringOrNull(offset)) as P; + case 2: + return (reader.readStringOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -352,6 +367,160 @@ extension TrackPreferenceQueryFilter }); } + QueryBuilder + prefsIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'prefs', + )); + }); + } + + QueryBuilder + prefsIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'prefs', + )); + }); + } + + QueryBuilder + prefsEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'prefs', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'prefs', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'prefs', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'prefs', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'prefs', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'prefs', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'prefs', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'prefs', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + prefsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'prefs', + value: '', + )); + }); + } + + QueryBuilder + prefsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'prefs', + value: '', + )); + }); + } + QueryBuilder syncIdIsNull() { return QueryBuilder.apply(this, (query) { @@ -602,6 +771,19 @@ extension TrackPreferenceQuerySortBy }); } + QueryBuilder sortByPrefs() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'prefs', Sort.asc); + }); + } + + QueryBuilder + sortByPrefsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'prefs', Sort.desc); + }); + } + QueryBuilder sortByUsername() { return QueryBuilder.apply(this, (query) { @@ -632,6 +814,19 @@ extension TrackPreferenceQuerySortThenBy }); } + QueryBuilder thenByPrefs() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'prefs', Sort.asc); + }); + } + + QueryBuilder + thenByPrefsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'prefs', Sort.desc); + }); + } + QueryBuilder thenBySyncId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'syncId', Sort.asc); @@ -669,6 +864,13 @@ extension TrackPreferenceQueryWhereDistinct }); } + QueryBuilder distinctByPrefs( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'prefs', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByUsername( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -691,6 +893,12 @@ extension TrackPreferenceQueryProperty }); } + QueryBuilder prefsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'prefs'); + }); + } + QueryBuilder usernameProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'username'); diff --git a/lib/models/track_search.dart b/lib/models/track_search.dart index 43b9723..8611809 100644 --- a/lib/models/track_search.dart +++ b/lib/models/track_search.dart @@ -1,13 +1,15 @@ class TrackSearch { int? id; + int? libraryId; + int? mediaId; int? syncId; String? title; - String? lastChapterRead; + int? lastChapterRead; int? totalChapter; @@ -33,6 +35,7 @@ class TrackSearch { TrackSearch( {this.id, + this.libraryId, this.mediaId, this.syncId, this.title, @@ -43,9 +46,9 @@ class TrackSearch { this.startedReadingDate, this.finishedReadingDate, this.trackingUrl, - this.coverUrl= '', - this.publishingStatus= '', - this.publishingType= '', - this.startDate= '', - this.summary= ''}); + this.coverUrl = '', + this.publishingStatus = '', + this.publishingType = '', + this.startDate = '', + this.summary = ''}); } diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 471eac3..bba6122 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -1616,7 +1616,7 @@ class _MangaDetailViewState extends ConsumerState maxHeight: mediaHeight(context, 0.9), minHeight: 80, child: Material( - color: Theme.of(context).scaffoldBackgroundColor, + color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9), borderRadius: BorderRadius.circular(20), clipBehavior: Clip.antiAliasWithSaveLayer, child: Padding( @@ -1641,7 +1641,7 @@ class _MangaDetailViewState extends ConsumerState ? TrackerWidget( mangaId: widget.manga!.id!, trackPreference: entries[index], - trackRes: trackRes[index], + trackRes: trackRes.first, ) : TrackListile( onTap: () async { diff --git a/lib/modules/manga/detail/providers/track_state_providers.dart b/lib/modules/manga/detail/providers/track_state_providers.dart index a6129f4..d444d11 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.dart @@ -1,7 +1,8 @@ import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/models/track_search.dart'; import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart'; -import 'package:mangayomi/services/myanimelist.dart'; +import 'package:mangayomi/services/trackers/anilist.dart'; +import 'package:mangayomi/services/trackers/myanimelist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'track_state_providers.g.dart'; @@ -13,20 +14,76 @@ class TrackState extends _$TrackState { } Future updateManga() async { - final updateTrack = await ref - .read(myAnimeListProvider(syncId: 1).notifier) - .updateManga(track!); + Track? updateTrack; + if (track!.syncId == 1) { + updateTrack = await ref + .read(myAnimeListProvider(syncId: track!.syncId!).notifier) + .updateManga(track!); + } else if (track!.syncId == 2) { + updateTrack = await ref + .read(anilistProvider(syncId: track!.syncId!).notifier) + .updateLibManga(track!); + } ref .read(tracksProvider(syncId: track!.syncId!).notifier) - .updateTrackManga(updateTrack); + .updateTrackManga(updateTrack!); + } + + int getScoreMaxValue() { + int? maxValue; + if (track!.syncId == 1) { + maxValue = 10; + } else if (track!.syncId == 2) { + maxValue = ref + .read(anilistProvider(syncId: track!.syncId!).notifier) + .getScoreValue() + .$1; + } + return maxValue!; + } + + String getTextMapper(String numberText) { + if (track!.syncId == 1) { + } else { + numberText = ref + .read(anilistProvider(syncId: 2).notifier) + .displayScore(int.parse(numberText)); + } + return numberText; + } + + int getScoreStep() { + int? step; + if (track!.syncId == 1) { + step = 1; + } else if (track!.syncId == 2) { + step = ref + .read(anilistProvider(syncId: track!.syncId!).notifier) + .getScoreValue() + .$2; + } + return step!; + } + + String displayScore(int score) { + String? result; + if (track!.syncId == 1) { + result = score.toString(); + } else { + result = + ref.read(anilistProvider(syncId: 2).notifier).displayScore(score); + } + return result; } Future setTrackSearch( TrackSearch trackSearch, int mangaId, int syncId) async { + Track? findManga; final track = Track( mangaId: mangaId, score: 0, + syncId: syncId, mediaId: trackSearch.mediaId, trackingUrl: trackSearch.trackingUrl, title: trackSearch.title, @@ -35,13 +92,26 @@ class TrackState extends _$TrackState { status: TrackStatus.planToRead, startedReadingDate: 0, finishedReadingDate: 0); - final findManga = await ref - .read(myAnimeListProvider(syncId: 1).notifier) - .findManga(track); - + if (syncId == 1) { + findManga = await ref + .read(myAnimeListProvider(syncId: syncId).notifier) + .findManga(track); + } else if (syncId == 2) { + findManga = findManga = await ref + .read(anilistProvider(syncId: syncId).notifier) + .findLibManga(track); + if (findManga == null) { + await ref + .read(anilistProvider(syncId: syncId).notifier) + .addLibManga(track); + findManga = await ref + .read(anilistProvider(syncId: syncId).notifier) + .findLibManga(track); + } + } ref .read(tracksProvider(syncId: syncId).notifier) - .updateTrackManga(findManga); + .updateTrackManga(findManga!); } List getStatusList() { @@ -49,8 +119,12 @@ class TrackState extends _$TrackState { List list = []; if (track!.syncId == 1) { statusList = ref - .read(myAnimeListProvider(syncId: 1).notifier) + .read(myAnimeListProvider(syncId: track!.syncId!).notifier) .myAnimeListStatusList; + } else if (track!.syncId == 2) { + statusList = ref + .read(anilistProvider(syncId: track!.syncId!).notifier) + .aniListStatusList; } for (var element in TrackStatus.values) { if (statusList.contains(element)) { @@ -64,8 +138,12 @@ class TrackState extends _$TrackState { Track? findManga; if (track!.syncId == 1) { findManga = await ref - .read(myAnimeListProvider(syncId: 1).notifier) + .read(myAnimeListProvider(syncId: track!.syncId!).notifier) .findManga(track!); + } else if (track!.syncId == 2) { + findManga = findManga = await ref + .read(anilistProvider(syncId: track!.syncId!).notifier) + .findLibManga(track!); } return findManga; } @@ -76,6 +154,10 @@ class TrackState extends _$TrackState { tracks = await ref .read(myAnimeListProvider(syncId: track!.syncId!).notifier) .search(query); + } else if (track!.syncId == 2) { + tracks = await ref + .read(anilistProvider(syncId: track!.syncId!).notifier) + .search(query); } return tracks; } diff --git a/lib/modules/manga/detail/providers/track_state_providers.g.dart b/lib/modules/manga/detail/providers/track_state_providers.g.dart index 4266e10..9e75022 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.g.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.g.dart @@ -6,7 +6,7 @@ part of 'track_state_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$trackStateHash() => r'493c91f74d44d2fcffd75bdbf3e434580fd5ac03'; +String _$trackStateHash() => r'7004528230213b458c7db417947eecfc700d44d7'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/manga/detail/widgets/tracker_widget.dart b/lib/modules/manga/detail/widgets/tracker_widget.dart index 31abe8d..5a3e55a 100644 --- a/lib/modules/manga/detail/widgets/tracker_widget.dart +++ b/lib/modules/manga/detail/widgets/tracker_widget.dart @@ -46,327 +46,370 @@ class _TrackerWidgetState extends ConsumerState { @override Widget build(BuildContext context) { - return Column( - children: [ - Row( - children: [ - Image.asset( - trackInfos(widget.trackPreference.syncId!).$1, - height: 30, - ), - Expanded( - child: _elevatedButton( - context, - onPressed: () async { - final trackSearch = await trackersSearchraggableMenu(context, - track: widget.trackRes) as TrackSearch?; - if (trackSearch != null) { - await ref - .read(trackStateProvider(track: null).notifier) - .setTrackSearch(trackSearch, widget.mangaId, - widget.trackPreference.syncId!); - } - }, - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - widget.trackRes.title!, - style: TextStyle( - color: secondaryColor(context), - fontSize: 16, - fontWeight: FontWeight.bold), - ), - ), + return Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(20)), + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Container( + color: const Color.fromRGBO(18, 25, 35, 1), + width: 70, + child: Image.asset( + trackInfos(widget.trackPreference.syncId!).$1, + height: 30, ), - IconButton( - onPressed: () { - ref - .read(tracksProvider( - syncId: widget.trackPreference.syncId!) - .notifier) - .deleteTrackManga(widget.trackRes); - }, - icon: const Icon(Icons.cancel_outlined)) - ], + ), ), ), - ) - ], - ), - Row( - children: [ - Expanded( - child: _elevatedButton(context, onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text( - "Status", + Expanded( + child: _elevatedButton( + context, + borderRadius: + const BorderRadius.only(topRight: Radius.circular(20)), + onPressed: () async { + final trackSearch = await trackersSearchraggableMenu( + context, + track: widget.trackRes) as TrackSearch?; + if (trackSearch != null) { + await ref + .read(trackStateProvider(track: null).notifier) + .setTrackSearch(trackSearch, widget.mangaId, + widget.trackPreference.syncId!); + } + }, + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + widget.trackRes.title!, + style: TextStyle( + color: secondaryColor(context), + fontSize: 16, + fontWeight: FontWeight.bold), + ), ), - content: SizedBox( - width: mediaWidth(context, 0.8), - child: ListView.builder( - shrinkWrap: true, - itemCount: ref - .read( - trackStateProvider(track: widget.trackRes) - .notifier) - .getStatusList() - .length, - itemBuilder: (context, index) { - final status = ref + ), + IconButton( + onPressed: () { + ref + .read(tracksProvider( + syncId: widget.trackPreference.syncId!) + .notifier) + .deleteTrackManga(widget.trackRes); + }, + icon: const Icon(Icons.cancel_outlined)) + ], + ), + ), + ) + ], + ), + Row( + children: [ + Expanded( + child: _elevatedButton(context, onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text( + "Status", + ), + content: SizedBox( + width: mediaWidth(context, 0.8), + child: ListView.builder( + shrinkWrap: true, + itemCount: ref .read(trackStateProvider( track: widget.trackRes) .notifier) - .getStatusList()[index]; - return RadioListTile( - dense: true, - contentPadding: const EdgeInsets.all(0), - value: status, - groupValue: widget.trackRes.status, - onChanged: (value) { - ref - .read(trackStateProvider( - track: widget.trackRes - ..status = status) - .notifier) - .updateManga(); - Navigator.pop(context); - }, - title: Text(getTrackStatus(status)), - ); - }, - )), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () async { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: - TextStyle(color: primaryColor(context)), - )), - ], - ) - ], - ); - }); - }, text: getTrackStatus(widget.trackRes.status)), - ), - Expanded( - child: _elevatedButton(context, onPressed: () { - int currentIntValue = widget.trackRes.lastChapterRead!; - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text( - "Chapters", - ), - content: StatefulBuilder( - builder: (context, setState) => SizedBox( - height: 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + .getStatusList() + .length, + itemBuilder: (context, index) { + final status = ref + .read(trackStateProvider( + track: widget.trackRes) + .notifier) + .getStatusList()[index]; + return RadioListTile( + dense: true, + contentPadding: const EdgeInsets.all(0), + value: status, + groupValue: widget.trackRes.status, + onChanged: (value) { + ref + .read(trackStateProvider( + track: widget.trackRes + ..status = status) + .notifier) + .updateManga(); + Navigator.pop(context); + }, + title: Text(getTrackStatus(status)), + ); + }, + )), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - NumberPicker( - value: currentIntValue, - minValue: 0, - maxValue: widget.trackRes.totalChapter != 0 - ? widget.trackRes.totalChapter! - : 10000, - step: 1, - haptics: true, - onChanged: (value) => - setState(() => currentIntValue = value), - ), + TextButton( + onPressed: () async { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: TextStyle( + color: primaryColor(context)), + )), ], + ) + ], + ); + }); + }, text: getTrackStatus(widget.trackRes.status)), + ), + Expanded( + child: _elevatedButton(context, onPressed: () { + int currentIntValue = widget.trackRes.lastChapterRead!; + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text( + "Chapters", + ), + content: StatefulBuilder( + builder: (context, setState) => SizedBox( + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NumberPicker( + value: currentIntValue, + minValue: 0, + maxValue: widget.trackRes.totalChapter != 0 + ? widget.trackRes.totalChapter! + : 10000, + step: 1, + haptics: true, + onChanged: (value) => + setState(() => currentIntValue = value), + ), + ], + ), ), ), - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () async { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: - TextStyle(color: primaryColor(context)), - )), - TextButton( - onPressed: () async { - ref - .read(trackStateProvider( - track: widget.trackRes - ..lastChapterRead = - currentIntValue) - .notifier) - .updateManga(); - Navigator.pop(context); - }, - child: Text( - "OK", - style: - TextStyle(color: primaryColor(context)), - )), - ], - ) - ], - ); - }); - }, - text: widget.trackRes.totalChapter != 0 - ? "${widget.trackRes.lastChapterRead}/${widget.trackRes.totalChapter}" - : "${widget.trackRes.lastChapterRead == 0 ? "Not Started" : widget.trackRes.lastChapterRead}"), - ), - Expanded( - child: _elevatedButton(context, onPressed: () { - int currentIntValue = widget.trackRes.score!; - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text( - "Score", - ), - content: StatefulBuilder( - builder: (context, setState) => SizedBox( - height: 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - NumberPicker( - value: currentIntValue, - minValue: 0, - maxValue: 10, - step: 1, - haptics: true, - onChanged: (value) => - setState(() => currentIntValue = value), - ), + TextButton( + onPressed: () async { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: TextStyle( + color: primaryColor(context)), + )), + TextButton( + onPressed: () async { + ref + .read(trackStateProvider( + track: widget.trackRes + ..lastChapterRead = + currentIntValue) + .notifier) + .updateManga(); + Navigator.pop(context); + }, + child: Text( + "OK", + style: TextStyle( + color: primaryColor(context)), + )), ], + ) + ], + ); + }); + }, + text: widget.trackRes.totalChapter != 0 + ? "${widget.trackRes.lastChapterRead}/${widget.trackRes.totalChapter}" + : "${widget.trackRes.lastChapterRead == 0 ? "Not Started" : widget.trackRes.lastChapterRead}"), + ), + Expanded( + child: _elevatedButton(context, onPressed: () { + int currentIntValue = widget.trackRes.score!; + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text( + "Score", + ), + content: StatefulBuilder( + builder: (context, setState) => SizedBox( + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NumberPicker( + value: currentIntValue, + minValue: 0, + maxValue: ref + .read(trackStateProvider( + track: widget.trackRes) + .notifier) + .getScoreMaxValue(), + textMapper: (numberText) { + return ref + .read(trackStateProvider( + track: widget.trackRes) + .notifier) + .getTextMapper(numberText); + }, + step: ref + .read(trackStateProvider( + track: widget.trackRes) + .notifier) + .getScoreStep(), + haptics: true, + onChanged: (value) => + setState(() => currentIntValue = value), + ), + ], + ), ), ), - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () async { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: - TextStyle(color: primaryColor(context)), - )), - TextButton( - onPressed: () async { - ref - .read(trackStateProvider( - track: widget.trackRes - ..score = currentIntValue) - .notifier) - .updateManga(); - Navigator.pop(context); - }, - child: Text( - "OK", - style: - TextStyle(color: primaryColor(context)), - )), - ], - ) - ], - ); - }); - }, - text: widget.trackRes.score != 0 - ? widget.trackRes.score.toString() - : "Score"), - ) - ], - ), - Row( - children: [ - Expanded( - child: _elevatedButton(context, onPressed: () async { - DateTime? newDate = await showDatePicker( - helpText: 'Start date', - locale: const Locale("fr", "FR"), - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime(2100)); - if (newDate == null) return; - ref - .read(trackStateProvider( - track: widget.trackRes - ..startedReadingDate = - newDate.millisecondsSinceEpoch) - .notifier) - .updateManga(); - }, - text: widget.trackRes.startedReadingDate != null && - widget.trackRes.startedReadingDate! > - DateTime(1970).millisecondsSinceEpoch - ? dateFormat( - widget.trackRes.startedReadingDate.toString(), - ref: ref, - useRelativeTimesTamps: false, - context: context) - : "Start date"), - ), - Expanded( - child: _elevatedButton(context, onPressed: () async { - DateTime? newDate = await showDatePicker( - helpText: 'Finish date', - locale: const Locale("fr", "FR"), - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime(2100)); - if (newDate == null) return; - ref - .read(trackStateProvider( - track: widget.trackRes - ..finishedReadingDate = - newDate.millisecondsSinceEpoch) - .notifier) - .updateManga(); - }, - text: widget.trackRes.finishedReadingDate != null && - widget.trackRes.finishedReadingDate! > - DateTime(1970).millisecondsSinceEpoch - ? dateFormat( - widget.trackRes.finishedReadingDate.toString(), - ref: ref, - useRelativeTimesTamps: false, - context: context) - : "Finish date"), - ) - ], - ), - ], + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: TextStyle( + color: primaryColor(context)), + )), + TextButton( + onPressed: () async { + ref + .read(trackStateProvider( + track: widget.trackRes + ..score = currentIntValue) + .notifier) + .updateManga(); + Navigator.pop(context); + }, + child: Text( + "OK", + style: TextStyle( + color: primaryColor(context)), + )), + ], + ) + ], + ); + }); + }, + text: widget.trackRes.score != 0 + ? ref + .read(trackStateProvider(track: widget.trackRes) + .notifier) + .displayScore(widget.trackRes.score!) + : "Score"), + ) + ], + ), + Row( + children: [ + Expanded( + child: _elevatedButton(context, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20)), onPressed: () async { + DateTime? newDate = await showDatePicker( + helpText: 'Start date', + locale: const Locale("fr", "FR"), + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100)); + if (newDate == null) return; + ref + .read(trackStateProvider( + track: widget.trackRes + ..startedReadingDate = + newDate.millisecondsSinceEpoch) + .notifier) + .updateManga(); + }, + text: widget.trackRes.startedReadingDate != null && + widget.trackRes.startedReadingDate! > + DateTime(1970).millisecondsSinceEpoch + ? dateFormat( + widget.trackRes.startedReadingDate.toString(), + ref: ref, + useRelativeTimesTamps: false, + context: context) + : "Start date"), + ), + Expanded( + child: _elevatedButton(context, + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(20)), onPressed: () async { + DateTime? newDate = await showDatePicker( + helpText: 'Finish date', + locale: const Locale("fr", "FR"), + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100)); + if (newDate == null) return; + ref + .read(trackStateProvider( + track: widget.trackRes + ..finishedReadingDate = + newDate.millisecondsSinceEpoch) + .notifier) + .updateManga(); + }, + text: widget.trackRes.finishedReadingDate != null && + widget.trackRes.finishedReadingDate! > + DateTime(1970).millisecondsSinceEpoch + ? dateFormat( + widget.trackRes.finishedReadingDate.toString(), + ref: ref, + useRelativeTimesTamps: false, + context: context) + : "Finish date"), + ) + ], + ), + ], + ), ); } } Widget _elevatedButton(BuildContext context, - {required VoidCallback onPressed, String text = "", Widget? child}) { + {required VoidCallback onPressed, + String text = "", + Widget? child, + BorderRadiusGeometry? borderRadius}) { return ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(0), @@ -375,7 +418,7 @@ Widget _elevatedButton(BuildContext context, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( side: BorderSide(width: 0.05, color: secondaryColor(context)), - borderRadius: BorderRadius.circular(0))), + borderRadius: borderRadius ?? BorderRadius.circular(0))), onPressed: onPressed, child: child ?? Text( diff --git a/lib/modules/more/settings/track/myanimelist/model.dart b/lib/modules/more/settings/track/myanimelist/model.dart index c57d6b8..b8624ae 100644 --- a/lib/modules/more/settings/track/myanimelist/model.dart +++ b/lib/modules/more/settings/track/myanimelist/model.dart @@ -1,13 +1,13 @@ -class MyAnimeListOAuth { +class OAuth { String? tokenType; int? expiresIn; String? accessToken; String? refreshToken; - MyAnimeListOAuth( + OAuth( {this.tokenType, this.expiresIn, this.accessToken, this.refreshToken}); - MyAnimeListOAuth.fromJson(Map json) { + OAuth.fromJson(Map json) { tokenType = json['token_type']; expiresIn = (json['expires_in'] as int) * 1000 + DateTime.now().millisecondsSinceEpoch; diff --git a/lib/modules/more/settings/track/providers/track_providers.dart b/lib/modules/more/settings/track/providers/track_providers.dart index 2c3b43b..df8e670 100644 --- a/lib/modules/more/settings/track/providers/track_providers.dart +++ b/lib/modules/more/settings/track/providers/track_providers.dart @@ -35,6 +35,7 @@ class Tracks extends _$Tracks { track.id = tra.first.id; } } + isar.writeTxnSync(() => isar.tracks.putSync(track..syncId = syncId)); } diff --git a/lib/modules/more/settings/track/track.dart b/lib/modules/more/settings/track/track.dart index 6883208..a1dc12d 100644 --- a/lib/modules/more/settings/track/track.dart +++ b/lib/modules/more/settings/track/track.dart @@ -4,7 +4,8 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/track_preference.dart'; import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart'; -import 'package:mangayomi/services/myanimelist.dart'; +import 'package:mangayomi/services/trackers/anilist.dart'; +import 'package:mangayomi/services/trackers/myanimelist.dart'; class TrackScreen extends ConsumerWidget { const TrackScreen({super.key}); @@ -32,7 +33,15 @@ class TrackScreen extends ConsumerWidget { .login(); }, id: 1, - entries: entries!) + entries: entries!), + TrackListile( + onTap: () async { + await ref + .read(anilistProvider(syncId: 2).notifier) + .login(); + }, + id: 2, + entries: entries) ], ); }), diff --git a/lib/modules/more/settings/track/widgets/track_listile.dart b/lib/modules/more/settings/track/widgets/track_listile.dart index 33bd298..bba19c4 100644 --- a/lib/modules/more/settings/track/widgets/track_listile.dart +++ b/lib/modules/more/settings/track/widgets/track_listile.dart @@ -22,13 +22,18 @@ class TrackListile extends ConsumerWidget { entries.where((element) => element.syncId == id).isNotEmpty; return ListTile( leading: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.asset( - trackInfos(id).$1, - height: 30, + borderRadius: BorderRadius.circular(20), + child: Container( + color: const Color.fromRGBO(18, 25, 35, 1), + width: 70, + child: Image.asset( + trackInfos(id).$1, + height: 30, + ), ), ), - trailing: trailing ?? (isLogged + trailing: trailing ?? + (isLogged ? const Icon( Icons.check, size: 30, @@ -75,4 +80,3 @@ class TrackListile extends ConsumerWidget { ); } } - diff --git a/lib/services/trackers/anilist.dart b/lib/services/trackers/anilist.dart new file mode 100644 index 0000000..173bd03 --- /dev/null +++ b/lib/services/trackers/anilist.dart @@ -0,0 +1,458 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:http/http.dart' as http; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/track.dart'; +import 'package:mangayomi/models/track_preference.dart'; +import 'dart:convert'; +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:riverpod_annotation/riverpod_annotation.dart'; +part 'anilist.g.dart'; + +@riverpod +class Anilist extends _$Anilist { + String clientId = + (Platform.isWindows || Platform.isLinux) ? '13587' : '13588'; + static const String baseApiUrl = "https://graphql.anilist.co/"; + static const String baseUrl = "https://anilist.co/api/v2/"; + static const String baseMangaUrl = "https://anilist.co/manga/"; + String redirectUri = (Platform.isWindows || Platform.isLinux) + ? 'http://localhost:43824/success?code=1337' + : 'mangayomi://success?code=1337'; + String clientSecret = (Platform.isWindows || Platform.isLinux) + ? 'tJA13cAR2tCCXrJCwwvmwEDbWRoIaahFiJTXToHd' + : 'G2fFUiGtgFd60D0lCkhgGKvMmrCfDmZXADQIzWXr'; + + @override + build({required int syncId}) {} + + String _authUrl() { + return 'https://anilist.co/api/v2/oauth/authorize?client_id=$clientId&redirect_uri=$redirectUri&response_type=code'; + } + + Future login() async { + final callbackUrlScheme = (Platform.isWindows || Platform.isLinux) + ? 'http://localhost:43824' + : 'mangayomi'; + final loginUrl = _authUrl(); + + try { + final uri = await FlutterWebAuth2.authenticate( + url: loginUrl, callbackUrlScheme: callbackUrlScheme); + + final code = Uri.parse(uri).queryParameters['code']; + final response = await http.post( + Uri.parse('https://anilist.co/api/v2/oauth/token'), + body: { + 'grant_type': 'authorization_code', + 'client_id': clientId, + 'client_secret': clientSecret, + 'redirect_uri': redirectUri, + 'code': code + }, + ); + final res = jsonDecode(response.body) as Map; + final aLOAuth = OAuth.fromJson(res); + final currenUser = await _getCurrentUser(aLOAuth.accessToken!); + ref.read(tracksProvider(syncId: syncId).notifier).login(TrackPreference( + syncId: syncId, + username: currenUser.$1, + prefs: jsonEncode({"scoreFormat": currenUser.$2}), + oAuth: jsonEncode(aLOAuth.toJson()))); + + return true; + } catch (e) { + return false; + } + } + + Future addLibManga(Track track) async { + final accessToken = await _getAccesToken(); + + const query = ''' + mutation AddManga(\$mangaId: Int, \$progress: Int, \$status: MediaListStatus) { + SaveMediaListEntry(mediaId: \$mangaId, progress: \$progress, status: \$status) { + id + status + } + } + '''; + + final body = { + "query": query, + "variables": { + "mangaId": track.mediaId, + "progress": track.lastChapterRead, + "status": toAniListStatus(track.status), + } + }; + + final response = await http.post( + Uri.parse(baseApiUrl), + headers: { + "Content-Type": "application/json", + 'Authorization': 'Bearer $accessToken' + }, + body: json.encode(body), + ); + final data = json.decode(response.body); + track.libraryId = data['data']['SaveMediaListEntry']['id']; + return track; + } + + Future updateLibManga(Track track) async { + final accessToken = await _getAccesToken(); + const query = ''' + mutation UpdateManga(\$listId: Int, \$progress: Int, \$status: MediaListStatus, \$score: Int, \$startedAt: FuzzyDateInput, \$completedAt: FuzzyDateInput) { + SaveMediaListEntry( + id: \$listId, + progress: \$progress, + status: \$status, + scoreRaw: \$score, + startedAt: \$startedAt, + completedAt: \$completedAt, + ) { + id + status + progress + } + } + '''; + + final body = { + "query": query, + "variables": { + "listId": track.libraryId, + "progress": track.lastChapterRead, + "status": toAniListStatus(track.status), + "score": track.score!, + "startedAt": createDate(track.startedReadingDate!), + "completedAt": createDate(track.finishedReadingDate!), + } + }; + + final dd = await http.post( + Uri.parse(baseApiUrl), + headers: { + "Content-Type": "application/json", + 'Authorization': 'Bearer $accessToken' + }, + body: json.encode(body), + ); + log(dd.body); + return track; + } + + Future> search(String search) async { + final accessToken = await _getAccesToken(); + const query = ''' + query Search(\$query: String) { + Page(perPage: 50) { + media(search: \$query, type: MANGA, format_not_in: [NOVEL]) { + id + title { + userPreferred + } + coverImage { + large + } + format + status + chapters + description + startDate { + year + month + day + } + } + } + } + '''; + + final body = { + "query": query, + "variables": { + "query": search, + } + }; + + final response = await http.post( + Uri.parse(baseApiUrl), + headers: { + "Content-Type": "application/json", + 'Authorization': 'Bearer $accessToken' + }, + body: json.encode(body), + ); + + final data = json.decode(response.body); + + final entries = + List>.from(data['data']['Page']['media']); + return entries + .map((jsonRes) => TrackSearch( + libraryId: jsonRes['id'], + syncId: syncId, + trackingUrl: "", + mediaId: jsonRes['id'], + summary: jsonRes['description'] ?? "", + totalChapter: jsonRes['chapters'] ?? 0, + coverUrl: jsonRes['coverImage']['large'] ?? "", + title: jsonRes['title']['userPreferred'], + startDate: jsonRes["start_date"] ?? + DateTime.fromMillisecondsSinceEpoch( + parseDate(jsonRes, 'startDate')) + .toString(), + publishingType: "", + publishingStatus: jsonRes['status'])) + .toList(); + } + + Future findLibManga( + Track track, + ) async { + final userId = ref.watch(tracksProvider(syncId: syncId))!.username; + + final accessToken = await _getAccesToken(); + const query = ''' + query(\$id: Int!, \$manga_id: Int!) { + Page { + mediaList(userId: \$id, type: MANGA, mediaId: \$manga_id) { + id + status + scoreRaw: score(format: POINT_100) + progress + startedAt { + year + month + day + } + completedAt { + year + month + day + } + media { + id + title { + userPreferred + } + coverImage { + large + } + format + status + chapters + description + startDate { + year + month + day + } + } + } + } + } + '''; + + final body = { + "query": query, + "variables": { + "id": int.parse(userId!), + "manga_id": track.mediaId, + } + }; + + final response = await http.post( + Uri.parse(baseApiUrl), + headers: { + "Content-Type": "application/json", + 'Authorization': 'Bearer $accessToken' + }, + body: json.encode(body), + ); + final data = json.decode(response.body); + final entries = + List>.from(data['data']['Page']['mediaList']); + if (entries.isNotEmpty) { + final jsonRes = entries.first; + track.libraryId = jsonRes['id']; + track.syncId = syncId; + track.mediaId = jsonRes['media']['id']; + track.status = _getALTrackStatus(jsonRes['status']); + track.title = jsonRes['media']['title']['userPreferred'] ?? ''; + track.score = jsonRes['scoreRaw'] ?? 0; + track.lastChapterRead = jsonRes['progress'] ?? 0; + track.startedReadingDate = parseDate(jsonRes, 'startedAt'); + track.finishedReadingDate = parseDate(jsonRes, 'completedAt'); + track.totalChapter = jsonRes['media']["chapters"] ?? 0; + } + return entries.isNotEmpty ? track : null; + } + + Future<(String, String)> _getCurrentUser(String accessToken) async { + const query = ''' + query User { + Viewer { + id + mediaListOptions { + scoreFormat + } + } + } + '''; + + final body = { + "query": query, + }; + + final response = await http.post( + Uri.parse(baseApiUrl), + headers: { + "Content-Type": "application/json", + 'Authorization': 'Bearer $accessToken' + }, + body: json.encode(body), + ); + final data = json.decode(response.body); + + final viewer = data['data']['Viewer']; + return ( + viewer['id'].toString(), + viewer['mediaListOptions']['scoreFormat'].toString() + ); + } + + Future _getAccesToken() async { + final track = ref.watch(tracksProvider(syncId: syncId)); + final mALOAuth = + OAuth.fromJson(jsonDecode(track!.oAuth!) as Map); + final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!); + if (DateTime.now().isAfter(expiresIn)) { + ref.read(tracksProvider(syncId: syncId).notifier).logout(); + throw Exception("Token expired"); + } + return mALOAuth.accessToken!; + } + + String _toAnilistScore(int score) { + final prefs = isar.trackPreferences.getSync(syncId)!.prefs; + final scoreFormat = jsonDecode(prefs!)['scoreFormat']; + return switch (scoreFormat) { + "POINT_10" => (score / 10).toString(), + "POINT_100" => score.toString(), + "POINT_5" => score == 0 + ? "0" + : score < 30 + ? "1" + : score < 50 + ? "2" + : score < 70 + ? "3" + : score < 90 + ? "4" + : "5", + "POINT_3" => score == 0 + ? "0" + : score <= 35 + ? ":(" + : score <= 60 + ? ":|" + : ":)", + "POINT_10_DECIMAL" => (score / 10).toString(), + _ => throw ("Unknown score type") + }; + } + + TrackStatus _getALTrackStatus(String status) { + return switch (status) { + "CURRENT" => TrackStatus.reading, + "COMPLETED" => TrackStatus.completed, + "PAUSED" => TrackStatus.onHold, + "DROPPED" => TrackStatus.dropped, + "PLANNING" => TrackStatus.planToRead, + _ => TrackStatus.rereading, + }; + } + + List aniListStatusList = [ + TrackStatus.reading, + TrackStatus.completed, + TrackStatus.onHold, + TrackStatus.dropped, + TrackStatus.planToRead, + TrackStatus.rereading + ]; + + String? toAniListStatus(TrackStatus status) { + return switch (status) { + TrackStatus.reading => "CURRENT", + TrackStatus.completed => "COMPLETED", + TrackStatus.onHold => "PAUSED", + TrackStatus.dropped => "DROPPED", + TrackStatus.planToRead => "PLANNING", + TrackStatus.rereading => "REPEATING", + }; + } + + int parseDate(Map json, String dateKey) { + try { + final year = json[dateKey]['year']; + final month = json[dateKey]['month']; + final day = json[dateKey]['day']; + final date = DateTime(year, month, day); + return date.millisecondsSinceEpoch; + } catch (_) { + return DateTime(1970, 01, 01).millisecondsSinceEpoch; + } + } + + Map createDate(int dateValue) { + if (dateValue == 0) { + return { + "year": null, + "month": null, + "day": null, + }; + } + + final date = DateTime.fromMillisecondsSinceEpoch(dateValue); + return { + "year": date.year, + "month": date.month, + "day": date.day, + }; + } + + String displayScore(int score) { + final prefs = isar.trackPreferences.getSync(syncId)!.prefs; + final scoreFormat = jsonDecode(prefs!)['scoreFormat']; + return switch (scoreFormat) { + 'POINT_5' => score == 0 ? "0 ★" : "${(score + 10) ~/ 20} ★", + 'POINT_3' => score == 0 + ? "-" + : score <= 35 + ? "😦" + : score <= 60 + ? "😐" + : "😊", + _ => _toAnilistScore(score), + }; + } + + (int, int) getScoreValue() { + final prefs = isar.trackPreferences.getSync(syncId)!.prefs; + String scoreFormat = jsonDecode(prefs!)['scoreFormat']; + return switch (scoreFormat) { + 'POINT_10' => (100, 10), + 'POINT_100' => (100, 1), + 'POINT_5' => (100, 20), + 'POINT_3' => (100, 30), + _ => (100, 1), + }; + } +} diff --git a/lib/services/trackers/anilist.g.dart b/lib/services/trackers/anilist.g.dart new file mode 100644 index 0000000..88aacdf --- /dev/null +++ b/lib/services/trackers/anilist.g.dart @@ -0,0 +1,124 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'anilist.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$anilistHash() => r'd09ca797106b31f075d5ded66ae138639b6ca745'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$Anilist extends BuildlessAutoDisposeNotifier { + late final int syncId; + + dynamic build({ + required int syncId, + }); +} + +/// See also [Anilist]. +@ProviderFor(Anilist) +const anilistProvider = AnilistFamily(); + +/// See also [Anilist]. +class AnilistFamily extends Family { + /// See also [Anilist]. + const AnilistFamily(); + + /// See also [Anilist]. + AnilistProvider call({ + required int syncId, + }) { + return AnilistProvider( + syncId: syncId, + ); + } + + @override + AnilistProvider getProviderOverride( + covariant AnilistProvider provider, + ) { + return call( + syncId: provider.syncId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'anilistProvider'; +} + +/// See also [Anilist]. +class AnilistProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [Anilist]. + AnilistProvider({ + required this.syncId, + }) : super.internal( + () => Anilist()..syncId = syncId, + from: anilistProvider, + name: r'anilistProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$anilistHash, + dependencies: AnilistFamily._dependencies, + allTransitiveDependencies: AnilistFamily._allTransitiveDependencies, + ); + + final int syncId; + + @override + bool operator ==(Object other) { + return other is AnilistProvider && other.syncId == syncId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, syncId.hashCode); + + return _SystemHash.finish(hash); + } + + @override + dynamic runNotifierBuild( + covariant Anilist notifier, + ) { + return notifier.build( + syncId: syncId, + ); + } +} +// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/services/myanimelist.dart b/lib/services/trackers/myanimelist.dart similarity index 92% rename from lib/services/myanimelist.dart rename to lib/services/trackers/myanimelist.dart index e76c422..52b1e16 100644 --- a/lib/services/myanimelist.dart +++ b/lib/services/trackers/myanimelist.dart @@ -38,13 +38,12 @@ class MyAnimeList extends _$MyAnimeList { if (queryParams['code'] == null) return null; final oAuth = await _getOAuth(queryParams['code']!); - final myAnimeListoAuth = - MyAnimeListOAuth.fromJson(oAuth as Map); - final username = await _getUserName(myAnimeListoAuth.accessToken!); + final mALOAuth = OAuth.fromJson(oAuth as Map); + final username = await _getUserName(mALOAuth.accessToken!); ref.read(tracksProvider(syncId: syncId).notifier).login(TrackPreference( syncId: syncId, username: username, - oAuth: jsonEncode(myAnimeListoAuth.toJson()))); + oAuth: jsonEncode(mALOAuth.toJson()))); return true; } catch (_) { @@ -54,28 +53,28 @@ class MyAnimeList extends _$MyAnimeList { Future _getAccesToken() async { final track = ref.watch(tracksProvider(syncId: syncId)); - final myAnimeListoAuth = MyAnimeListOAuth.fromJson( - jsonDecode(track!.oAuth!) as Map); - final expiresIn = - DateTime.fromMillisecondsSinceEpoch(myAnimeListoAuth.expiresIn!); + final mALOAuth = + OAuth.fromJson(jsonDecode(track!.oAuth!) as Map); + final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!); if (DateTime.now().isAfter(expiresIn)) { final params = { 'client_id': clientId, 'grant_type': 'refresh_token', - 'refresh_token': myAnimeListoAuth.refreshToken, + 'refresh_token': mALOAuth.refreshToken, }; final response = await http.post(Uri.parse('$baseOAuthUrl/token'), body: params); - final oAuth = MyAnimeListOAuth.fromJson( - jsonDecode(response.body) as Map); + final oAuth = + OAuth.fromJson(jsonDecode(response.body) as Map); final username = await _getUserName(oAuth.accessToken!); ref.read(tracksProvider(syncId: syncId).notifier).login(TrackPreference( syncId: syncId, username: username, + prefs: "", oAuth: jsonEncode(oAuth.toJson()))); return oAuth.accessToken!; } - return myAnimeListoAuth.accessToken!; + return mALOAuth.accessToken!; } Future> search(String query) async { @@ -250,4 +249,4 @@ class MyAnimeList extends _$MyAnimeList { final mJson = jsonDecode(await response.stream.bytesToString()); return _parseMangaItem(mJson, track); } -} \ No newline at end of file +} diff --git a/lib/services/myanimelist.g.dart b/lib/services/trackers/myanimelist.g.dart similarity index 97% rename from lib/services/myanimelist.g.dart rename to lib/services/trackers/myanimelist.g.dart index 00d8990..4af9478 100644 --- a/lib/services/myanimelist.g.dart +++ b/lib/services/trackers/myanimelist.g.dart @@ -6,7 +6,7 @@ part of 'myanimelist.dart'; // RiverpodGenerator // ************************************************************************** -String _$myAnimeListHash() => r'4fca14c944acc71eb514057708b8c60e28aa1744'; +String _$myAnimeListHash() => r'f96b3d08fd5c59b0811a7bf42ab13211903fdd5a'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/utils/constant.dart b/lib/utils/constant.dart index ed770be..18ff703 100644 --- a/lib/utils/constant.dart +++ b/lib/utils/constant.dart @@ -23,7 +23,7 @@ IconData getMangaStatusIcon(Status status) { Status.ongoing => Icons.schedule_rounded, Status.onHiatus => Icons.pause_circle_rounded, Status.canceled => Icons.cancel_rounded, - Status.completed => Icons.done, + Status.completed => Icons.done_all_outlined, Status.publishingFinished => Icons.done, _ => Icons.block_outlined, }; @@ -43,6 +43,6 @@ String getTrackStatus(TrackStatus status) { (String, String) trackInfos(int id) { return switch (id) { 1 => ("assets/tracker_mal.webp", "MyAnimeList"), - _ => ("", ""), + _ => ("assets/tracker_anilist.webp", "Anilist"), }; }