Feature : Added MyAnimeList tracker service
This commit is contained in:
parent
648efe00f5
commit
13b1fea0a3
38 changed files with 4418 additions and 65 deletions
|
|
@ -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
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
47
lib/models/track.dart
Normal 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
1686
lib/models/track.g.dart
Normal file
File diff suppressed because it is too large
Load diff
18
lib/models/track_preference.dart
Normal file
18
lib/models/track_preference.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
699
lib/models/track_preference.g.dart
Normal file
699
lib/models/track_preference.g.dart
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
51
lib/models/track_search.dart
Normal file
51
lib/models/track_search.dart
Normal 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= ''});
|
||||
}
|
||||
|
|
@ -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)))),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
125
lib/modules/manga/detail/providers/track_state_providers.g.dart
Normal file
125
lib/modules/manga/detail/providers/track_state_providers.g.dart
Normal 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
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
26
lib/modules/more/settings/track/myanimelist/model.dart
Normal file
26
lib/modules/more/settings/track/myanimelist/model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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!));
|
||||
}
|
||||
}
|
||||
124
lib/modules/more/settings/track/providers/track_providers.g.dart
Normal file
124
lib/modules/more/settings/track/providers/track_providers.g.dart
Normal 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
|
||||
41
lib/modules/more/settings/track/track.dart
Normal file
41
lib/modules/more/settings/track/track.dart
Normal 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!)
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib/modules/more/settings/track/widgets/track_listile.dart
Normal file
81
lib/modules/more/settings/track/widgets/track_listile.dart
Normal 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"),
|
||||
_ => ("", ""),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'l10n_providers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$l10nLocaleStateHash() => r'a1f842ccc55a111698bf7e096644858008014123';
|
||||
String _$l10nLocaleStateHash() => r'1c6cb9d6c0a56d54a6a5e7bc1b2dbc6c29538593';
|
||||
|
||||
/// See also [L10nLocaleState].
|
||||
@ProviderFor(L10nLocaleState)
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
294
lib/services/myanimelist.dart
Normal file
294
lib/services/myanimelist.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
125
lib/services/myanimelist.g.dart
Normal file
125
lib/services/myanimelist.g.dart
Normal 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
|
||||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'headers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$headersHash() => r'bb9238fcab606b6fe4ae2d2dab249820d7b4e2ff';
|
||||
String _$headersHash() => r'e154417a4c4e9416c52cb13060ffb04a07fd489e';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
40
pubspec.lock
40
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue