Feature : Added MyAnimeList tracker service

This commit is contained in:
kodjomoustapha 2023-07-15 17:35:00 +01:00
parent 648efe00f5
commit 13b1fea0a3
38 changed files with 4418 additions and 65 deletions

View file

@ -29,6 +29,16 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mangayomi" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

BIN
assets/tracker_mal.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

47
lib/models/track.dart Normal file
View file

@ -0,0 +1,47 @@
import 'package:isar/isar.dart';
part 'track.g.dart';
@collection
@Name("Track")
class Track {
Id? id;
int? mediaId;
int? mangaId;
int? syncId;
String? title;
int ?lastChapterRead;
int? totalChapter;
int? score;
@enumerated
TrackStatus status;
int? startedReadingDate;
int? finishedReadingDate;
String? trackingUrl;
Track(
{this.id = Isar.autoIncrement,
this.mediaId,
this.mangaId,
this.syncId,
this.title,
this.lastChapterRead,
this.totalChapter,
this.score,
required this.status,
this.startedReadingDate,
this.finishedReadingDate,
this.trackingUrl});
}
enum TrackStatus { reading, completed, onHold, dropped, planToRead, rereading }

1686
lib/models/track.g.dart Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
import 'package:isar/isar.dart';
part 'track_preference.g.dart';
@collection
@Name("Track Preference")
class TrackPreference {
Id? syncId;
String? username;
String? oAuth;
TrackPreference({
this.syncId,
this.username,
this.oAuth,
});
}

View file

@ -0,0 +1,699 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'track_preference.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetTrackPreferenceCollection on Isar {
IsarCollection<TrackPreference> get trackPreferences => this.collection();
}
const TrackPreferenceSchema = CollectionSchema(
name: r'Track Preference',
id: -7260395670212271073,
properties: {
r'oAuth': PropertySchema(
id: 0,
name: r'oAuth',
type: IsarType.string,
),
r'username': PropertySchema(
id: 1,
name: r'username',
type: IsarType.string,
)
},
estimateSize: _trackPreferenceEstimateSize,
serialize: _trackPreferenceSerialize,
deserialize: _trackPreferenceDeserialize,
deserializeProp: _trackPreferenceDeserializeProp,
idName: r'syncId',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _trackPreferenceGetId,
getLinks: _trackPreferenceGetLinks,
attach: _trackPreferenceAttach,
version: '3.1.0+1',
);
int _trackPreferenceEstimateSize(
TrackPreference object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.oAuth;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.username;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _trackPreferenceSerialize(
TrackPreference object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.oAuth);
writer.writeString(offsets[1], object.username);
}
TrackPreference _trackPreferenceDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = TrackPreference(
oAuth: reader.readStringOrNull(offsets[0]),
syncId: id,
username: reader.readStringOrNull(offsets[1]),
);
return object;
}
P _trackPreferenceDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readStringOrNull(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _trackPreferenceGetId(TrackPreference object) {
return object.syncId ?? Isar.autoIncrement;
}
List<IsarLinkBase<dynamic>> _trackPreferenceGetLinks(TrackPreference object) {
return [];
}
void _trackPreferenceAttach(
IsarCollection<dynamic> col, Id id, TrackPreference object) {
object.syncId = id;
}
extension TrackPreferenceQueryWhereSort
on QueryBuilder<TrackPreference, TrackPreference, QWhere> {
QueryBuilder<TrackPreference, TrackPreference, QAfterWhere> anySyncId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension TrackPreferenceQueryWhere
on QueryBuilder<TrackPreference, TrackPreference, QWhereClause> {
QueryBuilder<TrackPreference, TrackPreference, QAfterWhereClause>
syncIdEqualTo(Id syncId) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: syncId,
upper: syncId,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterWhereClause>
syncIdNotEqualTo(Id syncId) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: syncId, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: syncId, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: syncId, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: syncId, includeUpper: false),
);
}
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterWhereClause>
syncIdGreaterThan(Id syncId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: syncId, includeLower: include),
);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterWhereClause>
syncIdLessThan(Id syncId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: syncId, includeUpper: include),
);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterWhereClause>
syncIdBetween(
Id lowerSyncId,
Id upperSyncId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: lowerSyncId,
includeLower: includeLower,
upper: upperSyncId,
includeUpper: includeUpper,
));
});
}
}
extension TrackPreferenceQueryFilter
on QueryBuilder<TrackPreference, TrackPreference, QFilterCondition> {
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'oAuth',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'oAuth',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'oAuth',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'oAuth',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'oAuth',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthBetween(
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'oAuth',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'oAuth',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'oAuth',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'oAuth',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'oAuth',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'oAuth',
value: '',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
oAuthIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'oAuth',
value: '',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
syncIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'syncId',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
syncIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'syncId',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
syncIdEqualTo(Id? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'syncId',
value: value,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
syncIdGreaterThan(
Id? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'syncId',
value: value,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
syncIdLessThan(
Id? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'syncId',
value: value,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
syncIdBetween(
Id? lower,
Id? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'syncId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'username',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'username',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'username',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'username',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'username',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameBetween(
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'username',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'username',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'username',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'username',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'username',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'username',
value: '',
));
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterFilterCondition>
usernameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'username',
value: '',
));
});
}
}
extension TrackPreferenceQueryObject
on QueryBuilder<TrackPreference, TrackPreference, QFilterCondition> {}
extension TrackPreferenceQueryLinks
on QueryBuilder<TrackPreference, TrackPreference, QFilterCondition> {}
extension TrackPreferenceQuerySortBy
on QueryBuilder<TrackPreference, TrackPreference, QSortBy> {
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy> sortByOAuth() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'oAuth', Sort.asc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
sortByOAuthDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'oAuth', Sort.desc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
sortByUsername() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'username', Sort.asc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
sortByUsernameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'username', Sort.desc);
});
}
}
extension TrackPreferenceQuerySortThenBy
on QueryBuilder<TrackPreference, TrackPreference, QSortThenBy> {
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy> thenByOAuth() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'oAuth', Sort.asc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
thenByOAuthDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'oAuth', Sort.desc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy> thenBySyncId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncId', Sort.asc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
thenBySyncIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncId', Sort.desc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
thenByUsername() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'username', Sort.asc);
});
}
QueryBuilder<TrackPreference, TrackPreference, QAfterSortBy>
thenByUsernameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'username', Sort.desc);
});
}
}
extension TrackPreferenceQueryWhereDistinct
on QueryBuilder<TrackPreference, TrackPreference, QDistinct> {
QueryBuilder<TrackPreference, TrackPreference, QDistinct> distinctByOAuth(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'oAuth', caseSensitive: caseSensitive);
});
}
QueryBuilder<TrackPreference, TrackPreference, QDistinct> distinctByUsername(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'username', caseSensitive: caseSensitive);
});
}
}
extension TrackPreferenceQueryProperty
on QueryBuilder<TrackPreference, TrackPreference, QQueryProperty> {
QueryBuilder<TrackPreference, int, QQueryOperations> syncIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'syncId');
});
}
QueryBuilder<TrackPreference, String?, QQueryOperations> oAuthProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'oAuth');
});
}
QueryBuilder<TrackPreference, String?, QQueryOperations> usernameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'username');
});
}
}

View file

@ -0,0 +1,51 @@
class TrackSearch {
int? id;
int? mediaId;
int? syncId;
String? title;
String? lastChapterRead;
int? totalChapter;
double? score;
String? status;
int? startedReadingDate;
int? finishedReadingDate;
String? trackingUrl;
String? coverUrl;
String? summary;
String? publishingStatus;
String? publishingType;
String? startDate;
TrackSearch(
{this.id,
this.mediaId,
this.syncId,
this.title,
this.lastChapterRead,
this.totalChapter,
this.score,
this.status = '',
this.startedReadingDate,
this.finishedReadingDate,
this.trackingUrl,
this.coverUrl= '',
this.publishingStatus= '',
this.publishingType= '',
this.startDate= '',
this.summary= ''});
}

View file

@ -13,11 +13,20 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.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/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/myanimelist.dart';
import 'package:mangayomi/sources/utils/utils.dart';
import 'package:mangayomi/utils/cached_network.dart';
import 'package:mangayomi/utils/colors.dart';
import 'package:mangayomi/utils/constant.dart';
import 'package:mangayomi/utils/date.dart';
import 'package:mangayomi/utils/headers.dart';
import 'package:mangayomi/utils/media_query.dart';
import 'package:mangayomi/utils/utils.dart';
@ -30,6 +39,7 @@ import 'package:mangayomi/modules/manga/detail/widgets/chapter_sort_list_tile_wi
import 'package:mangayomi/modules/manga/download/providers/download_provider.dart';
import 'package:mangayomi/modules/widgets/error_text.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:numberpicker/numberpicker.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:share_plus/share_plus.dart';
@ -1286,11 +1296,64 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
widget.action!,
const SizedBox(
width: 5,
),
StreamBuilder(
stream: isar.trackPreferences
.filter()
.syncIdIsNotNull()
.watch(fireImmediately: true),
builder: (context, snapshot) {
List<TrackPreference>? entries =
snapshot.hasData ? snapshot.data! : [];
if (entries.isEmpty) {
return Container();
}
return SizedBox(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
elevation: 0),
onPressed: () {
_trackingDialog(entries);
},
child: StreamBuilder(
stream: isar.tracks
.filter()
.idIsNotNull()
.mangaIdEqualTo(widget.manga!.id!)
.watch(fireImmediately: true),
builder: (context, snapshot) {
List<Track>? trackRes =
snapshot.hasData ? snapshot.data : [];
bool isNotEmpty = trackRes!.isNotEmpty;
Color color = isNotEmpty
? primaryColor(context)
: secondaryColor(context);
return Column(
children: [
Icon(
isNotEmpty
? Icons.done
: Icons.screen_rotation_alt_rounded,
size: 22,
color: color,
),
const SizedBox(
height: 4,
),
Text(
isNotEmpty
? '${trackRes.length} traker'
: 'Tracking',
style: TextStyle(fontSize: 13, color: color),
),
],
);
}),
),
);
}),
SizedBox(
width: isTablet(context) ? null : mediaWidth(context, 0.4),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
@ -1549,4 +1612,770 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
);
});
}
_trackingDialog(List<TrackPreference>? entries) {
DraggableMenu.open(
context,
DraggableMenu(
ui: SoftModernDraggableMenu(radius: 20, barItem: Container()),
maxHeight: mediaHeight(context, 0.9),
minHeight: 80,
child: Material(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(20),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: ListView.separated(
padding: const EdgeInsets.all(0),
itemCount: entries!.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return StreamBuilder(
stream: isar.tracks
.filter()
.idIsNotNull()
.syncIdEqualTo(entries[index].syncId)
.mangaIdEqualTo(widget.manga!.id!)
.watch(fireImmediately: true),
builder: (context, snapshot) {
List<Track>? trackRes =
snapshot.hasData ? snapshot.data : [];
return trackRes!.isNotEmpty
? Container(
decoration: BoxDecoration(border: Border.all()),
child: Column(
children: [
Row(
children: [
Image.asset(
"assets/tracker_mal.webp",
height: 30,
),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: RoundedRectangleBorder(
side: BorderSide(
color:
primaryColor(context),
width: 0.2),
borderRadius:
BorderRadius.circular(
0))),
onPressed: () async {
final trackSearch =
await trackersSearchraggableMenu(
syncId: entries[index]
.syncId!,
query: trackRes
.first.title!)
as TrackSearch?;
if (trackSearch != null) {
await ref
.read(trackStateProvider(
track: null)
.notifier)
.setTrackSearch(
trackSearch,
widget.manga!.id!,
entries[index].syncId!);
}
},
child: Row(
children: [
Expanded(
child: Padding(
padding:
const EdgeInsets.all(8),
child: Text(
trackRes.first.title!,
style: TextStyle(
color: secondaryColor(
context),fontSize: 16,
fontWeight:
FontWeight.bold),
),
),
),
IconButton(
onPressed: () {
ref
.read(tracksProvider(
syncId: entries[
index]
.syncId!)
.notifier)
.deleteTrackManga(
trackRes.first);
},
icon: const Icon(
Icons.cancel_outlined))
],
),
),
)
],
),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: primaryColor(
context),
width: 0.2),
borderRadius:
BorderRadius.circular(
0))),
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: trackRes
.first)
.notifier)
.getStatusList()
.length,
itemBuilder:
(context,
index) {
final status = ref
.read(trackStateProvider(
track:
trackRes.first)
.notifier)
.getStatusList()[index];
return RadioListTile(
dense: true,
contentPadding:
const EdgeInsets
.all(0),
value: status,
groupValue:
trackRes
.first
.status,
onChanged:
(value) {
ref
.read(trackStateProvider(
track: trackRes.first..status = status)
.notifier)
.updateItem();
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)),
)),
],
)
],
);
});
},
child: Text(getTrackStatus(
trackRes.first.status))),
),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: primaryColor(
context),
width: 0.2),
borderRadius:
BorderRadius.circular(
0))),
onPressed: () {
int currentIntValue = trackRes
.first.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: trackRes
.first
.totalChapter !=
0
? trackRes
.first
.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: trackRes.first..lastChapterRead = currentIntValue)
.notifier)
.updateItem();
Navigator.pop(
context);
},
child: Text(
"OK",
style: TextStyle(
color: primaryColor(
context)),
)),
],
)
],
);
});
},
child: Text(trackRes
.first.totalChapter !=
0
? "${trackRes.first.lastChapterRead}/${trackRes.first.totalChapter}"
: "${trackRes.first.lastChapterRead == 0 ? "Not Started" : trackRes.first.lastChapterRead}")),
),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: primaryColor(
context),
width: 0.2),
borderRadius:
BorderRadius.circular(
0))),
onPressed: () {
int currentIntValue =
trackRes.first.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: 10,
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: trackRes.first..score = currentIntValue)
.notifier)
.updateItem();
Navigator.pop(
context);
},
child: Text(
"OK",
style: TextStyle(
color: primaryColor(
context)),
)),
],
)
],
);
});
},
child: Text(
trackRes.first.score != 0
? trackRes.first.score
.toString()
: "Score")),
)
],
),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
side: BorderSide(
color:
primaryColor(context),
width: 0.2),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
0))),
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: trackRes.first
..startedReadingDate =
newDate
.millisecondsSinceEpoch)
.notifier)
.updateItem();
},
child: Text(trackRes.first
.startedReadingDate !=
null &&
trackRes.first
.startedReadingDate! >
DateTime(1970)
.millisecondsSinceEpoch
? dateFormat(
trackRes.first
.startedReadingDate
.toString(),
ref: ref,
useRelativeTimesTamps: false,
context: context)
: "Start date")),
),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: primaryColor(
context),
width: 0.2),
borderRadius:
BorderRadius.circular(
0))),
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: trackRes.first
..startedReadingDate =
newDate
.millisecondsSinceEpoch)
.notifier)
.updateItem();
},
child: Text(trackRes.first
.finishedReadingDate !=
null &&
trackRes.first.finishedReadingDate! >
DateTime(1970)
.millisecondsSinceEpoch
? dateFormat(
trackRes.first
.finishedReadingDate
.toString(),
ref: ref,
useRelativeTimesTamps: false,
context: context)
: "Finish date")),
)
],
),
],
),
)
: TrackListile(
onTap: () async {
final trackSearch =
await trackersSearchraggableMenu(
syncId: entries[index].syncId!,
query: widget.manga!.name!)
as TrackSearch?;
if (trackSearch != null) {
await ref
.read(trackStateProvider(track: null)
.notifier)
.setTrackSearch(
trackSearch,
widget.manga!.id!,
entries[index].syncId!);
}
},
id: entries[index].syncId!,
entries: const []);
});
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
),
),
));
}
trackersSearchraggableMenu(
{required int syncId, required String query}) async {
return await DraggableMenu.open(
context,
DraggableMenu(
blockMenuClosing: true,
ui: SoftModernDraggableMenu(
radius: 20,
barItem: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20))),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.clear)),
)
],
),
)),
maxHeight: mediaHeight(context, 0.9),
child: TrackerWidgetSearch(
query: query,
syncId: syncId,
)));
}
}
class TrackerWidgetSearch extends ConsumerStatefulWidget {
final int syncId;
final String query;
const TrackerWidgetSearch(
{required this.syncId, required this.query, super.key});
@override
ConsumerState<TrackerWidgetSearch> createState() =>
_TrackerWidgetSearchState();
}
class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
@override
initState() {
_init();
super.initState();
}
late List<TrackSearch> tracks = [];
_init() async {
await Future.delayed(const Duration(microseconds: 100));
tracks = await ref
.read(myAnimeListProvider(syncId: widget.syncId).notifier)
.search(widget.query);
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
late String query = widget.query;
late final _controller = TextEditingController(text: query);
bool _isLoading = true;
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: _isLoading
? const ProgressCenter()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: SizedBox(
height: mediaHeight(context, 0.8),
child: Column(
children: [
Flexible(
child: ListView.separated(
padding: const EdgeInsets.only(top: 20),
itemCount: tracks.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(top: 5),
child: InkWell(
onTap: () {
Navigator.pop(context, tracks[index]);
},
child: Column(
children: [
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Material(
borderRadius: BorderRadius.circular(5),
color: Colors.transparent,
clipBehavior:
Clip.antiAliasWithSaveLayer,
child: Ink.image(
height: 120,
width: 80,
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
tracks[index].coverUrl!),
),
),
const SizedBox(
width: 10,
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SizedBox(
width: mediaWidth(context, 0.6),
child: Text(
tracks[index].title!,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
),
Row(
children: [
const Text(
"Type : ",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12),
),
Text(
tracks[index].publishingType!,
style: const TextStyle(
fontSize: 12),
),
],
),
Row(
children: [
const Text(
"Status : ",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12),
),
Text(
tracks[index].publishingStatus!,
style: const TextStyle(
fontSize: 12),
),
],
),
],
)
],
),
Text(
tracks[index].summary!,
style: const TextStyle(
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
maxLines: 3,
),
],
),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.text,
onChanged: (d) {
setState(() {
query = d;
});
},
onFieldSubmitted: (d) async {
setState(() {
_isLoading = true;
});
tracks = await ref
.read(myAnimeListProvider(syncId: widget.syncId)
.notifier)
.search(d);
if (mounted) {
setState(() {
_isLoading = false;
});
}
},
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: Colors.transparent,
suffixIcon: query.isEmpty
? null
: IconButton(
onPressed: () {
_controller.clear();
},
icon: const Icon(Icons.clear)),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: primaryColor(context)),
),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: primaryColor(context)),
),
border: OutlineInputBorder(
borderSide:
BorderSide(color: primaryColor(context)))),
),
),
],
),
),
),
);
}
}

View file

@ -122,7 +122,6 @@ class _MangaDetailsViewState extends ConsumerState<MangaDetailsView> {
),
action: widget.manga.favorite
? SizedBox(
width: isTablet(context) ? null : mediaWidth(context, 0.4),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
@ -154,46 +153,43 @@ class _MangaDetailsViewState extends ConsumerState<MangaDetailsView> {
),
),
)
: SizedBox(
width: isTablet(context) ? null : mediaWidth(context, 0.4),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
elevation: 0),
onPressed: () {
final checkCategoryList =
isar.categorys.filter().idIsNotNull().isNotEmptySync();
if (checkCategoryList) {
_openCategory(widget.manga);
} else {
final model = widget.manga;
isar.writeTxnSync(() {
model.favorite = true;
model.dateAdded = DateTime.now().millisecondsSinceEpoch;
isar.mangas.putSync(model);
});
}
},
child: Column(
children: [
Icon(
Icons.favorite_border_rounded,
size: 22,
color: secondaryColor(context),
),
const SizedBox(
height: 4,
),
Text(
l10n.add_to_library,
style: TextStyle(
color: secondaryColor(context), fontSize: 13),
)
],
: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
elevation: 0),
onPressed: () {
final checkCategoryList =
isar.categorys.filter().idIsNotNull().isNotEmptySync();
if (checkCategoryList) {
_openCategory(widget.manga);
} else {
final model = widget.manga;
isar.writeTxnSync(() {
model.favorite = true;
model.dateAdded = DateTime.now().millisecondsSinceEpoch;
isar.mangas.putSync(model);
});
}
},
child: Column(
children: [
Icon(
Icons.favorite_border_rounded,
size: 22,
color: secondaryColor(context),
),
),
const SizedBox(
height: 4,
),
Text(
l10n.add_to_library,
style: TextStyle(
color: secondaryColor(context), fontSize: 13),
)
],
),
),
manga: widget.manga,
isExtended: (value) {
ref.read(isExtendedStateProvider.notifier).update(value);

View file

@ -0,0 +1,63 @@
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:riverpod_annotation/riverpod_annotation.dart';
part 'track_state_providers.g.dart';
@riverpod
class TrackState extends _$TrackState {
@override
Track build({Track? track}) {
return track!;
}
Future updateItem() async {
final updateTrack = await ref
.read(myAnimeListProvider(syncId: 1).notifier)
.updateItem(track!);
ref
.read(tracksProvider(syncId: track!.syncId!).notifier)
.updateTrackManga(updateTrack);
}
Future setTrackSearch(
TrackSearch trackSearch, int mangaId, int syncId) async {
final track = Track(
mangaId: mangaId,
score: 0,
mediaId: trackSearch.mediaId,
trackingUrl: trackSearch.trackingUrl,
title: trackSearch.title,
lastChapterRead: 0,
totalChapter: 0,
status: TrackStatus.planToRead,
startedReadingDate: 0,
finishedReadingDate: 0);
final findTrack = await ref
.read(myAnimeListProvider(syncId: 1).notifier)
.findListItem(track);
ref
.read(tracksProvider(syncId: syncId).notifier)
.updateTrackManga(findTrack);
}
List<TrackStatus> getStatusList() {
List<TrackStatus> statusList = [];
List<TrackStatus> list = [];
if (track!.syncId == 1) {
statusList = ref
.read(myAnimeListProvider(syncId: 1).notifier)
.myAnimeListStatusList;
}
for (var element in TrackStatus.values) {
if (statusList.contains(element)) {
list.add(element);
}
}
return list;
}
}

View file

@ -0,0 +1,125 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'track_state_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$trackStateHash() => r'3e7b916624f8035766d9a6408812bf1cc1247915';
/// 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 _$TrackState extends BuildlessAutoDisposeNotifier<Track> {
late final Track? track;
Track build({
Track? track,
});
}
/// See also [TrackState].
@ProviderFor(TrackState)
const trackStateProvider = TrackStateFamily();
/// See also [TrackState].
class TrackStateFamily extends Family<Track> {
/// See also [TrackState].
const TrackStateFamily();
/// See also [TrackState].
TrackStateProvider call({
Track? track,
}) {
return TrackStateProvider(
track: track,
);
}
@override
TrackStateProvider getProviderOverride(
covariant TrackStateProvider provider,
) {
return call(
track: provider.track,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'trackStateProvider';
}
/// See also [TrackState].
class TrackStateProvider
extends AutoDisposeNotifierProviderImpl<TrackState, Track> {
/// See also [TrackState].
TrackStateProvider({
this.track,
}) : super.internal(
() => TrackState()..track = track,
from: trackStateProvider,
name: r'trackStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$trackStateHash,
dependencies: TrackStateFamily._dependencies,
allTransitiveDependencies:
TrackStateFamily._allTransitiveDependencies,
);
final Track? track;
@override
bool operator ==(Object other) {
return other is TrackStateProvider && other.track == track;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, track.hashCode);
return _SystemHash.finish(hash);
}
@override
Track runNotifierBuild(
covariant TrackState notifier,
) {
return notifier.build(
track: track,
);
}
}
// 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

View file

@ -1,5 +1,4 @@
import 'dart:io';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/providers/storage_provider.dart';

View file

@ -41,6 +41,11 @@ class SettingsScreen extends StatelessWidget {
subtitle: l10n.downloads_subtitle,
icon: Icons.download_outlined,
onTap: () => context.push('/downloads')),
ListTileWidget(
title: "Tracking",
subtitle: "",
icon: Icons.screen_rotation_alt_rounded,
onTap: () => context.push('/track')),
ListTileWidget(
title: l10n.browse,
subtitle: l10n.browse_subtitle,

View file

@ -0,0 +1,26 @@
class MyAnimeListOAuth {
String? tokenType;
int? expiresIn;
String? accessToken;
String? refreshToken;
MyAnimeListOAuth(
{this.tokenType, this.expiresIn, this.accessToken, this.refreshToken});
MyAnimeListOAuth.fromJson(Map<String, dynamic> json) {
tokenType = json['token_type'];
expiresIn = (json['expires_in'] as int) * 1000 +
DateTime.now().millisecondsSinceEpoch;
accessToken = json['access_token'];
refreshToken = json['refresh_token'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['token_type'] = tokenType;
data['expires_in'] = expiresIn;
data['access_token'] = accessToken;
data['refresh_token'] = refreshToken;
return data;
}
}

View file

@ -0,0 +1,44 @@
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'track_providers.g.dart';
@riverpod
class Tracks extends _$Tracks {
@override
TrackPreference? build({required int? syncId}) {
return isar.trackPreferences.getSync(syncId!);
}
void login(TrackPreference trackPreference) {
isar.writeTxnSync(() {
isar.trackPreferences.putSync(trackPreference);
});
}
void logout() {
isar.writeTxnSync(() {
isar.trackPreferences.deleteSync(syncId!);
});
}
void updateTrackManga(Track track) {
final tra = isar.tracks
.filter()
.syncIdEqualTo(syncId)
.mangaIdEqualTo(track.mangaId)
.findAllSync();
if (tra.isNotEmpty) {
if (tra.first.mediaId != track.mangaId) {
track.id = tra.first.id;
}
}
isar.writeTxnSync(() => isar.tracks.putSync(track..syncId = syncId));
}
void deleteTrackManga(Track track) {
isar.writeTxnSync(() => isar.tracks.deleteSync(track.id!));
}
}

View file

@ -0,0 +1,124 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'track_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$tracksHash() => r'11a1c74c458db7f5b790de1451d239662cec1ed3';
/// 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 _$Tracks extends BuildlessAutoDisposeNotifier<TrackPreference?> {
late final int? syncId;
TrackPreference? build({
required int? syncId,
});
}
/// See also [Tracks].
@ProviderFor(Tracks)
const tracksProvider = TracksFamily();
/// See also [Tracks].
class TracksFamily extends Family<TrackPreference?> {
/// See also [Tracks].
const TracksFamily();
/// See also [Tracks].
TracksProvider call({
required int? syncId,
}) {
return TracksProvider(
syncId: syncId,
);
}
@override
TracksProvider getProviderOverride(
covariant TracksProvider provider,
) {
return call(
syncId: provider.syncId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tracksProvider';
}
/// See also [Tracks].
class TracksProvider
extends AutoDisposeNotifierProviderImpl<Tracks, TrackPreference?> {
/// See also [Tracks].
TracksProvider({
required this.syncId,
}) : super.internal(
() => Tracks()..syncId = syncId,
from: tracksProvider,
name: r'tracksProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tracksHash,
dependencies: TracksFamily._dependencies,
allTransitiveDependencies: TracksFamily._allTransitiveDependencies,
);
final int? syncId;
@override
bool operator ==(Object other) {
return other is TracksProvider && 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
TrackPreference? runNotifierBuild(
covariant Tracks 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

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
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';
class TrackScreen extends ConsumerWidget {
const TrackScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text("Tracking"),
),
body: StreamBuilder(
stream: isar.trackPreferences
.filter()
.syncIdIsNotNull()
.watch(fireImmediately: true),
builder: (context, snapshot) {
List<TrackPreference>? entries =
snapshot.hasData ? snapshot.data : [];
return Column(
children: [
TrackListile(
onTap: () async {
await ref
.read(myAnimeListProvider(syncId: 1).notifier)
.login();
},
id: 1,
entries: entries!)
],
);
}),
);
}
}

View file

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
class TrackListile extends ConsumerWidget {
final VoidCallback onTap;
final int id;
final List<TrackPreference> entries;
const TrackListile(
{super.key,
required this.onTap,
required this.id,
required this.entries});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isLogged =
entries.where((element) => element.syncId == id).isNotEmpty;
return ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
_track(id).$1,
height: 30,
),
),
trailing: isLogged
? const Icon(
Icons.check,
size: 30,
color: Colors.green,
)
: null,
onTap: isLogged
? () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(
"Log out from ${_track(id).$2}",
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel")),
const SizedBox(
width: 15,
),
TextButton(
onPressed: () {
ref
.read(tracksProvider(syncId: id).notifier)
.logout();
Navigator.pop(context);
},
child: const Text("Log out")),
],
)
],
);
});
}
: onTap,
title: Text(_track(id).$2),
);
}
}
(String, String) _track(int id) {
return switch (id) {
1 => ("assets/tracker_mal.webp", "MyAnimeList"),
_ => ("", ""),
};
}

View file

@ -19,12 +19,12 @@ class ListTileWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
onTap: onTap,
subtitle: subtitle != null
? Text(
subtitle!,
style: TextStyle(fontSize: 11, color: secondaryColor(context)),
)
: null,
// subtitle: subtitle != null
// ? Text(
// subtitle!,
// style: TextStyle(fontSize: 11, color: secondaryColor(context)),
// )
// : null,
leading: SizedBox(
height: 40,
child: Icon(

View file

@ -6,7 +6,7 @@ part of 'l10n_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$l10nLocaleStateHash() => r'a1f842ccc55a111698bf7e096644858008014123';
String _$l10nLocaleStateHash() => r'1c6cb9d6c0a56d54a6a5e7bc1b2dbc6c29538593';
/// See also [L10nLocaleState].
@ProviderFor(L10nLocaleState)

View file

@ -10,6 +10,8 @@ import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:path/path.dart' as path;
@ -103,7 +105,9 @@ class StorageProvider {
HistorySchema,
DownloadSchema,
SourceSchema,
SettingsSchema
SettingsSchema,
TrackPreferenceSchema,
TrackSchema
], directory: dir!.path, name: "mangayomiDb", inspector: inspector!);
if (isar.settings.filter().idEqualTo(227).isEmptySync()) {

View file

@ -5,6 +5,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/browse/sources/sources_filter_screen.dart';
import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart';
import 'package:mangayomi/modules/more/settings/track/track.dart';
import 'package:mangayomi/modules/updates/updates_screen.dart';
import 'package:mangayomi/modules/webview/webview.dart';
import 'package:mangayomi/modules/browse/browse_screen.dart';
@ -236,6 +237,19 @@ class RouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/track",
name: "track",
builder: (context, state) {
return const TrackScreen();
},
pageBuilder: (context, state) {
return CustomTransition(
key: state.pageKey,
child: const TrackScreen(),
);
},
),
GoRoute(
path: "/sourceFilter",
name: "sourceFilter",

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:dart_eval/dart_eval_bridge.dart';
import 'package:dart_eval/stdlib/core.dart';
import 'package:mangayomi/eval/bridge_class/manga_model.dart';
import 'package:mangayomi/eval/bridge_class/model.dart';
import 'package:mangayomi/eval/compiler/compiler.dart';
import 'package:mangayomi/main.dart';
@ -16,8 +17,6 @@ import 'package:mangayomi/sources/utils/utils.dart';
import 'package:mangayomi/utils/reg_exp_matcher.dart';
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../eval/bridge_class/manga_model.dart';
part 'get_chapter_url.g.dart';
class GetChapterUrlModel {
@ -52,11 +51,12 @@ Future<GetChapterUrlModel> getChapterUrl(
final storageProvider = StorageProvider();
path = await storageProvider.getMangaChapterDirectory(chapter);
final mangaDirectory = await storageProvider.getMangaMainDirectory(chapter);
final source =
getSource(chapter.manga.value!.lang!, chapter.manga.value!.source!);
List<Uint8List?> archiveImages = [];
final isLocalArchive = (chapter.archivePath ?? '').isNotEmpty;
if (!chapter.manga.value!.isLocalArchive!) {
final source =
getSource(chapter.manga.value!.lang!, chapter.manga.value!.source!);
if (isarPageUrls.isNotEmpty &&
isarPageUrls.first.urls != null &&
isarPageUrls.first.urls!.isNotEmpty) {

View file

@ -6,7 +6,7 @@ part of 'get_chapter_url.dart';
// RiverpodGenerator
// **************************************************************************
String _$getChapterUrlHash() => r'46fc82b7ddeb8f6f1658dbc942db69f651505aad';
String _$getChapterUrlHash() => r'289384e134e65a4c0a78724d3ea4b98a605e2a52';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -0,0 +1,294 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.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:riverpod_annotation/riverpod_annotation.dart';
part 'myanimelist.g.dart';
@riverpod
class MyAnimeList extends _$MyAnimeList {
String baseOAuthUrl = 'https://myanimelist.net/v1/oauth2';
String baseApiUrl = 'https://api.myanimelist.net/v2';
String codeVerifier = "";
String clientId = (Platform.isWindows || Platform.isLinux)
? '39e9be346b4e7dbcc59a98357e2f8472'
: '0c9100ccd443ddb441a319a881180f7f';
int listPaginationAmount = 250;
@override
build({required int syncId}) {}
Future<bool?> 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 queryParams = Uri.parse(uri).queryParameters;
if (queryParams['code'] == null) return null;
final oAuth = await _getOAuth(queryParams['code']!);
final myAnimeListoAuth =
MyAnimeListOAuth.fromJson(oAuth as Map<String, dynamic>);
final username = await _getUserName(myAnimeListoAuth.accessToken!);
ref.read(tracksProvider(syncId: syncId).notifier).login(TrackPreference(
syncId: syncId,
username: username,
oAuth: jsonEncode(myAnimeListoAuth.toJson())));
return true;
} catch (_) {
return false;
}
}
Future<String> _getAccesToken() async {
final track = ref.watch(tracksProvider(syncId: syncId));
final myAnimeListoAuth = MyAnimeListOAuth.fromJson(
jsonDecode(track!.oAuth!) as Map<String, dynamic>);
final expiresIn =
DateTime.fromMillisecondsSinceEpoch(myAnimeListoAuth.expiresIn!);
if (DateTime.now().isAfter(expiresIn)) {
final params = {
'client_id': clientId,
'grant_type': 'refresh_token',
'refresh_token': myAnimeListoAuth.refreshToken,
};
final response =
await http.post(Uri.parse('$baseOAuthUrl/token'), body: params);
final oAuth = MyAnimeListOAuth.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,
oAuth: jsonEncode(oAuth.toJson())));
return oAuth.accessToken!;
}
return myAnimeListoAuth.accessToken!;
}
Future<List<TrackSearch>> search(String query) async {
final accessToken = await _getAccesToken();
final url = Uri.parse('$baseApiUrl/manga').replace(queryParameters: {
'q': query.trim(),
'nsfw': 'true',
});
final result =
await http.get(url, headers: {'Authorization': 'Bearer $accessToken'});
final res = jsonDecode(result.body) as Map<String, dynamic>;
List<int> mangaIds =
(res['data'] as List).map((e) => e['node']["id"] as int).toList();
List<TrackSearch> trackSearchResult = [];
for (var mangaId in mangaIds) {
final trackSearch = await getMangaDetails(mangaId, accessToken);
trackSearchResult.add(trackSearch);
}
return trackSearchResult
.where((element) => !element.publishingType!.contains("novel"))
.toList();
}
Future<TrackSearch> getMangaDetails(int id, String accessToken) async {
final url = Uri.parse('$baseApiUrl/manga/$id').replace(queryParameters: {
'fields':
'id,title,synopsis,num_chapters,main_picture,status,media_type,start_date',
});
final result =
await http.get(url, headers: {'Authorization': 'Bearer $accessToken'});
final res = jsonDecode(result.body) as Map<String, dynamic>;
return TrackSearch(
mediaId: res["id"],
summary: res["synopsis"] ?? "",
totalChapter: res["num_chapters"],
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/manga/${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) {
return switch (status) {
"reading" => TrackStatus.reading,
"completed" => TrackStatus.completed,
"on_hold" => TrackStatus.onHold,
"dropped" => TrackStatus.dropped,
"plan_to_read" => TrackStatus.planToRead,
_ => TrackStatus.rereading,
};
}
List<TrackStatus> myAnimeListStatusList = [
TrackStatus.reading,
TrackStatus.completed,
TrackStatus.onHold,
TrackStatus.dropped,
TrackStatus.planToRead,
TrackStatus.rereading
];
String? toMyAnimeListStatus(TrackStatus status) {
return switch (status) {
TrackStatus.reading => "reading",
TrackStatus.completed => "completed",
TrackStatus.onHold => "on_hold",
TrackStatus.dropped => "dropped",
TrackStatus.planToRead => "plan_to_read",
TrackStatus.rereading => "reading",
};
}
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 http.get(Uri.parse('$baseApiUrl/users/@me'),
headers: {'Authorization': 'Bearer $accessToken'});
return jsonDecode(response.body)['name'];
}
Future<Track> findListItem(Track track) async {
final accessToken = await _getAccesToken();
final uri = Uri.parse('$baseApiUrl/manga/${track.mediaId}')
.replace(queryParameters: {
'fields': 'num_chapters,my_list_status{start_date,finish_date}',
});
final response =
await http.get(uri, headers: {'Authorization': 'Bearer $accessToken'});
final mJson = jsonDecode(response.body);
track.totalChapter = mJson['num_chapters'] ?? 0;
if (mJson['my_list_status'] != null) {
track = parseMangaItem(mJson["my_list_status"], track);
} else {
track = await updateItem(track);
}
return track;
}
Future<List<TrackSearch>> findListItems(String query,
{int offset = 0}) async {
final accessToken = await _getAccesToken();
final json = await getListPage(offset);
final obj = json['data'] as List<dynamic>;
List<int> mangaIds = obj
.where((data) => data['node']['title']
.toString()
.toLowerCase()
.contains(query.toLowerCase()))
.map((data) => data['node']['id'] as int)
.toList();
List<TrackSearch> trackSearchResult = [];
for (var mangaId in mangaIds) {
final trackSearch = await getMangaDetails(mangaId, accessToken);
trackSearchResult.add(trackSearch);
}
if (json['paging']['next'] != null) {
final newV =
await findListItems(query, offset: offset + listPaginationAmount);
trackSearchResult.addAll(newV);
}
return trackSearchResult;
}
Future<Map<String, dynamic>> getListPage(int offset) async {
final urlBuilder =
Uri.parse('$baseApiUrl/users/@me/mangalist').replace(queryParameters: {
'fields': 'list_status{start_date,finish_date}',
'limit': listPaginationAmount.toString(),
});
if (offset > 0) {
urlBuilder.queryParameters['offset'] = offset.toString();
}
final url = urlBuilder.toString();
final response =
await http.get(Uri.parse(url), headers: {'X-MAL-CLIENT-ID': clientId});
return jsonDecode(response.body);
}
Track parseMangaItem(Map<String, dynamic> mJson, Track track) {
bool isRereading = mJson["is_rereading"] ?? false;
track.status = isRereading
? TrackStatus.rereading
: _getMALTrackStatus(mJson["status"]);
track.lastChapterRead = int.parse(mJson["num_chapters_read"].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> updateItem(Track track) async {
final accessToken = await _getAccesToken();
final formBody = {
'status': (toMyAnimeListStatus(track.status) ?? 'reading').toString(),
'is_rereading': (track.status == TrackStatus.rereading).toString(),
'score': track.score.toString(),
'num_chapters_read': track.lastChapterRead.toString(),
if (track.startedReadingDate != null)
'start_date': _convertToIsoDate(track.startedReadingDate),
if (track.finishedReadingDate != null)
'finish_date': _convertToIsoDate(track.finishedReadingDate)
};
final request = http.Request(
'PUT', Uri.parse('$baseApiUrl/manga/${track.mediaId}/my_list_status'));
request.bodyFields = formBody;
request.headers.addAll({'Authorization': 'Bearer $accessToken'});
final response = await http.Client().send(request);
final mJson = jsonDecode(await response.stream.bytesToString());
return parseMangaItem(mJson, track);
}
}

View file

@ -0,0 +1,125 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'myanimelist.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$myAnimeListHash() => r'd3ec65023fe7f920fad11afa4866072fb75ee257';
/// 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 _$MyAnimeList extends BuildlessAutoDisposeNotifier<dynamic> {
late final int syncId;
dynamic build({
required int syncId,
});
}
/// See also [MyAnimeList].
@ProviderFor(MyAnimeList)
const myAnimeListProvider = MyAnimeListFamily();
/// See also [MyAnimeList].
class MyAnimeListFamily extends Family<dynamic> {
/// See also [MyAnimeList].
const MyAnimeListFamily();
/// See also [MyAnimeList].
MyAnimeListProvider call({
required int syncId,
}) {
return MyAnimeListProvider(
syncId: syncId,
);
}
@override
MyAnimeListProvider getProviderOverride(
covariant MyAnimeListProvider provider,
) {
return call(
syncId: provider.syncId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'myAnimeListProvider';
}
/// See also [MyAnimeList].
class MyAnimeListProvider
extends AutoDisposeNotifierProviderImpl<MyAnimeList, dynamic> {
/// See also [MyAnimeList].
MyAnimeListProvider({
required this.syncId,
}) : super.internal(
() => MyAnimeList()..syncId = syncId,
from: myAnimeListProvider,
name: r'myAnimeListProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$myAnimeListHash,
dependencies: MyAnimeListFamily._dependencies,
allTransitiveDependencies:
MyAnimeListFamily._allTransitiveDependencies,
);
final int syncId;
@override
bool operator ==(Object other) {
return other is MyAnimeListProvider && 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 MyAnimeList 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

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
const defaultUserAgent =
@ -27,3 +28,15 @@ IconData getMangaStatusIcon(Status status) {
_ => Icons.block_outlined,
};
}
String getTrackStatus(TrackStatus status) {
return switch (status) {
TrackStatus.reading => "Reading",
TrackStatus.completed => "Completed",
TrackStatus.onHold => "On Hold",
TrackStatus.dropped => "Dropped",
TrackStatus.planToRead => "Plan To Read",
TrackStatus.rereading => "Rereading",
};
}

View file

@ -11,18 +11,21 @@ part 'headers.g.dart';
Map<String, String> headers(HeadersRef ref,
{required String source, required String lang}) {
final sourceM = getSource(lang, source);
if (sourceM.headers!.isEmpty) {
if (sourceM.headers!.isEmpty && !sourceM.hasCloudflare!) {
return {};
}
Map<String, String> newHeaders = {};
final headers = jsonDecode(sourceM.headers!) as Map;
newHeaders =
headers.map((key, value) => MapEntry(key.toString(), value.toString()));
if (sourceM.headers!.isNotEmpty) {
final headers = jsonDecode(sourceM.headers!) as Map;
newHeaders =
headers.map((key, value) => MapEntry(key.toString(), value.toString()));
}
if (sourceM.hasCloudflare!) {
final userAgent = isar.settings.getSync(227)!.userAgent!;
final cookie = ref.watch(cookieStateProvider(source));
newHeaders.addAll({'User-Agent': userAgent, "Cookie": cookie});
}
return newHeaders;
}

View file

@ -6,7 +6,7 @@ part of 'headers.dart';
// RiverpodGenerator
// **************************************************************************
String _$headersHash() => r'bb9238fcab606b6fe4ae2d2dab249820d7b4e2ff';
String _$headersHash() => r'e154417a4c4e9416c52cb13060ffb04a07fd489e';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -1,7 +1,7 @@
completeLanguageName(String lang) {
lang = lang.toLowerCase();
for (var element in languagesMap.entries) {
if (element.value.toString().toLowerCase() == lang) {
if (element.value.toLowerCase() == lang) {
return element.key;
}
}
@ -64,5 +64,4 @@ final languagesMap = {
"čeština": "cs",
"Kurdî": "ku",
"Magyar": "hu",
"polski": "pl",
};

View file

@ -10,6 +10,7 @@
#include <flutter_js/flutter_js_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
@ -24,4 +25,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_to_front_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin");
window_to_front_plugin_register_with_registrar(window_to_front_registrar);
}

View file

@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_js
isar_flutter_libs
url_launcher_linux
window_to_front
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View file

@ -8,21 +8,25 @@ import Foundation
import desktop_webview_window
import flutter_inappwebview
import flutter_js
import flutter_web_auth_2
import isar_flutter_libs
import package_info_plus
import path_provider_foundation
import share_plus
import sqflite
import url_launcher_macos
import window_to_front
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
}

View file

@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View file

@ -504,6 +504,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_auth_2:
dependency: "direct main"
description:
name: flutter_web_auth_2
sha256: "70e4df72940183b8e269c4163f78dd5bf9102ba3329bfe00c0f2373f30fb32d0"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
flutter_web_auth_2_platform_interface:
dependency: transitive
description:
name: flutter_web_auth_2_platform_interface
sha256: f6fa7059ff3428c19cd756c02fef8eb0147131c7e64591f9060c90b5ab84f094
url: "https://pub.dev"
source: hosted
version: "2.1.4"
flutter_web_plugins:
dependency: transitive
description: flutter
@ -621,6 +637,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.0"
infinite_listview:
dependency: transitive
description:
name: infinite_listview
sha256: f6062c1720eb59be553dfa6b89813d3e8dd2f054538445aaa5edaddfa5195ce6
url: "https://pub.dev"
source: hosted
version: "1.1.0"
intl:
dependency: "direct main"
description:
@ -757,6 +781,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
numberpicker:
dependency: "direct main"
description:
name: numberpicker
sha256: "4c129154944b0f6b133e693f8749c3f8bfb67c4d07ef9dcab48b595c22d1f156"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
octo_image:
dependency: transitive
description:
@ -1314,6 +1346,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.4"
window_to_front:
dependency: transitive
description:
name: window_to_front
sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
xdg_directories:
dependency: transitive
description:

View file

@ -47,6 +47,8 @@ dependencies:
dart_eval: ^0.6.0
json_path: ^0.6.0
bot_toast: ^4.0.4
flutter_web_auth_2: ^2.1.5
numberpicker: ^2.1.2
cupertino_icons: ^1.0.2

View file

@ -12,6 +12,7 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_to_front/window_to_front_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopWebviewWindowPluginRegisterWithRegistrar(
@ -26,4 +27,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowToFrontPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowToFrontPlugin"));
}

View file

@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
share_plus
url_launcher_windows
window_to_front
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST