Merge pull request #497 from Schnitzel5/enhance/two-way-tracker

improved two-way track screen
This commit is contained in:
Moustapha Kodjo Amadou 2025-06-25 11:37:21 +01:00 committed by GitHub
commit 73e029aad0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1299 additions and 541 deletions

View file

@ -477,5 +477,6 @@
"hwdec": "Hardware Decoder",
"track_library_add": "Add to local library",
"track_library_add_confirm": "Add tracked item to local library",
"track_library_not_logged": "Login to the corresponding tracker to use this feature!"
"track_library_not_logged": "Login to the corresponding tracker to use this feature!",
"track_library_switch": "Switch to another tracker"
}

View file

@ -2987,6 +2987,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Login to the corresponding tracker to use this feature!'**
String get track_library_not_logged;
/// No description provided for @track_library_switch.
///
/// In en, this message translates to:
/// **'Switch to another tracker'**
String get track_library_switch;
}
class _AppLocalizationsDelegate

View file

@ -1527,4 +1527,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1538,4 +1538,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1527,4 +1527,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1544,6 +1544,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).

View file

@ -1549,4 +1549,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1533,4 +1533,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1543,4 +1543,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1541,6 +1541,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}
/// The translations for Portuguese, as used in Brazil (`pt_BR`).

View file

@ -1543,4 +1543,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1528,4 +1528,7 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1534,4 +1534,7 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -1496,4 +1496,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get track_library_not_logged =>
'Login to the corresponding tracker to use this feature!';
@override
String get track_library_switch => 'Switch to another tracker';
}

View file

@ -9,12 +9,15 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/more/data_and_storage/providers/storage_usage.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
@ -57,6 +60,8 @@ void main(List<String> args) async {
}
}
isar = await StorageProvider().initDB(null, inspector: kDebugMode);
await Hive.initFlutter();
Hive.registerAdapter(TrackSearchAdapter());
runApp(const ProviderScope(child: MyApp()));
unawaited(_postLaunchInit()); // Defer non-essential async operations

View file

@ -242,6 +242,8 @@ class Settings {
bool? clearChapterCacheOnAppLaunch;
String? lastTrackerLibraryLocation;
Settings({
this.id = 227,
this.displayType = DisplayType.compactGrid,
@ -349,6 +351,7 @@ class Settings {
this.mangaExtensionsRepo,
this.animeExtensionsRepo,
this.novelExtensionsRepo,
this.lastTrackerLibraryLocation,
});
Settings.fromJson(Map<String, dynamic> json) {
@ -562,6 +565,7 @@ class Settings {
.map((e) => Repo.fromJson(e))
.toList();
}
lastTrackerLibraryLocation = json['lastTrackerLibraryLocation'];
}
Map<String, dynamic> toJson() => {
@ -692,6 +696,7 @@ class Settings {
'mangaExtensionsRepo': mangaExtensionsRepo?.map((e) => e.toJson()).toList(),
'animeExtensionsRepo': animeExtensionsRepo?.map((e) => e.toJson()).toList(),
'novelExtensionsRepo': novelExtensionsRepo?.map((e) => e.toJson()).toList(),
'lastTrackerLibraryLocation': lastTrackerLibraryLocation,
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,57 @@
import 'package:hive/hive.dart';
part 'track_search.g.dart';
@HiveType(typeId: 0, adapterName: "TrackSearchAdapter")
class TrackSearch {
@HiveField(0)
int? id;
@HiveField(1)
int? libraryId;
@HiveField(2)
int? mediaId;
@HiveField(3)
int? syncId;
@HiveField(4)
String? title;
@HiveField(5)
int? lastChapterRead;
@HiveField(6)
int? totalChapter;
@HiveField(7)
double? score;
@HiveField(8)
String? status;
@HiveField(9)
int? startedReadingDate;
@HiveField(10)
int? finishedReadingDate;
@HiveField(11)
String? trackingUrl;
@HiveField(12)
String? coverUrl;
@HiveField(13)
String? summary;
@HiveField(14)
String? publishingStatus;
@HiveField(15)
String? publishingType;
@HiveField(16)
String? startDate;
TrackSearch({
@ -52,4 +73,38 @@ class TrackSearch {
this.startDate = '',
this.summary = '',
});
TrackSearch.fromJson(Map<String, dynamic> json) {
id = json['id'];
libraryId = json['libraryId'];
mediaId = json['mediaId'];
syncId = json['syncId'];
title = json['title'];
lastChapterRead = json['lastChapterRead'];
totalChapter = json['totalChapter'];
score = json['score'];
status = json['status'];
startedReadingDate = json['startedReadingDate'];
finishedReadingDate = json['finishedReadingDate'];
trackingUrl = json['trackingUrl'];
coverUrl = json['coverUrl'];
publishingStatus = json['publishingStatus'];
}
Map<String, dynamic> toJson() => {
'id': id,
'libraryId': libraryId,
'mediaId': mediaId,
'syncId': syncId,
'title': title,
'lastChapterRead': lastChapterRead,
'totalChapter': totalChapter,
'score': score,
'status': status,
'startedReadingDate': startedReadingDate,
'finishedReadingDate': finishedReadingDate,
'trackingUrl': trackingUrl,
'coverUrl': coverUrl,
'publishingStatus': publishingStatus,
};
}

View file

@ -0,0 +1,89 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'track_search.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class TrackSearchAdapter extends TypeAdapter<TrackSearch> {
@override
final int typeId = 0;
@override
TrackSearch read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return TrackSearch(
id: fields[0] as int?,
libraryId: fields[1] as int?,
mediaId: fields[2] as int?,
syncId: fields[3] as int?,
title: fields[4] as String?,
lastChapterRead: fields[5] as int?,
totalChapter: fields[6] as int?,
score: fields[7] as double?,
status: fields[8] as String?,
startedReadingDate: fields[9] as int?,
finishedReadingDate: fields[10] as int?,
trackingUrl: fields[11] as String?,
coverUrl: fields[12] as String?,
publishingStatus: fields[14] as String?,
publishingType: fields[15] as String?,
startDate: fields[16] as String?,
summary: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, TrackSearch obj) {
writer
..writeByte(17)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.libraryId)
..writeByte(2)
..write(obj.mediaId)
..writeByte(3)
..write(obj.syncId)
..writeByte(4)
..write(obj.title)
..writeByte(5)
..write(obj.lastChapterRead)
..writeByte(6)
..write(obj.totalChapter)
..writeByte(7)
..write(obj.score)
..writeByte(8)
..write(obj.status)
..writeByte(9)
..write(obj.startedReadingDate)
..writeByte(10)
..write(obj.finishedReadingDate)
..writeByte(11)
..write(obj.trackingUrl)
..writeByte(12)
..write(obj.coverUrl)
..writeByte(13)
..write(obj.summary)
..writeByte(14)
..write(obj.publishingStatus)
..writeByte(15)
..write(obj.publishingType)
..writeByte(16)
..write(obj.startDate);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TrackSearchAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View file

@ -329,39 +329,13 @@ class _MainScreenState extends ConsumerState<MainScreen> {
),
);
}
if (dest.contains("/trackerLibrary/anilist")) {
destinations[dest.indexOf(
"/trackerLibrary/anilist",
)] = NavigationRailDestination(
if (dest.contains("/trackerLibrary")) {
destinations[dest.indexOf("/trackerLibrary")] = NavigationRailDestination(
selectedIcon: const Icon(Icons.account_tree),
icon: const Icon(Icons.account_tree_outlined),
label: Padding(
padding: const EdgeInsets.only(top: 5),
child: Text("AL"),
),
);
}
if (dest.contains("/trackerLibrary/kitsu")) {
destinations[dest.indexOf(
"/trackerLibrary/kitsu",
)] = NavigationRailDestination(
selectedIcon: const Icon(Icons.account_tree),
icon: const Icon(Icons.account_tree_outlined),
label: Padding(
padding: const EdgeInsets.only(top: 5),
child: Text("Kitsu"),
),
);
}
if (dest.contains("/trackerLibrary/mal")) {
destinations[dest.indexOf(
"/trackerLibrary/mal",
)] = NavigationRailDestination(
selectedIcon: const Icon(Icons.account_tree),
icon: const Icon(Icons.account_tree_outlined),
label: Padding(
padding: const EdgeInsets.only(top: 5),
child: Text("MAL"),
child: Text(l10n.tracking),
),
);
}
@ -448,29 +422,11 @@ class _MainScreenState extends ConsumerState<MainScreen> {
label: l10n.more,
);
}
if (dest.contains("/trackerLibrary/anilist")) {
destinations[dest.indexOf(
"/trackerLibrary/anilist",
)] = NavigationDestination(
if (dest.contains("/trackerLibrary")) {
destinations[dest.indexOf("/trackerLibrary")] = NavigationDestination(
selectedIcon: const Icon(Icons.account_tree),
icon: const Icon(Icons.account_tree_outlined),
label: "AL",
);
}
if (dest.contains("/trackerLibrary/kitsu")) {
destinations[dest.indexOf(
"/trackerLibrary/kitsu",
)] = NavigationDestination(
selectedIcon: const Icon(Icons.account_tree),
icon: const Icon(Icons.account_tree_outlined),
label: "Kitsu",
);
}
if (dest.contains("/trackerLibrary/mal")) {
destinations[dest.indexOf("/trackerLibrary/mal")] = NavigationDestination(
selectedIcon: const Icon(Icons.account_tree),
icon: const Icon(Icons.account_tree_outlined),
label: "MAL",
label: l10n.tracking,
);
}
@ -592,9 +548,7 @@ class _TabletLayout extends StatelessWidget {
'/updates',
'/browse',
'/more',
'/trackerLibrary/anilist',
'/trackerLibrary/kitsu',
'/trackerLibrary/mal',
'/trackerLibrary',
};
return (location == null || validLocations.contains(location)) ? 100 : 0;
@ -662,9 +616,7 @@ class _MobileBottomNavigation extends StatelessWidget {
'/updates',
'/browse',
'/more',
'/trackerLibrary/anilist',
'/trackerLibrary/kitsu',
'/trackerLibrary/mal',
'/trackerLibrary',
};
return (location == null || validLocations.contains(location)) ? null : 0;

View file

@ -1,7 +1,10 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
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/modules/tracker_library/tracker_library_screen.dart';
import 'package:mangayomi/services/trackers/anilist.dart';
import 'package:mangayomi/services/trackers/kitsu.dart';
import 'package:mangayomi/services/trackers/myanimelist.dart';
@ -156,3 +159,26 @@ class TrackState extends _$TrackState {
return await tracker.fetchUserData(isManga: _isManga);
}
}
@riverpod
class LastTrackerLibraryLocationState
extends _$LastTrackerLibraryLocationState {
@override
(int, bool) build() {
final value = isar.settings.getSync(227)!.lastTrackerLibraryLocation;
if (value != null) {
final data = value.split(",");
return (int.parse(data[0]), bool.parse(data[1]));
}
return (TrackerProviders.myAnimeList.syncId, false);
}
void set((int, bool) value) {
final settings = isar.settings.getSync(227);
final val = "${value.$1},${value.$2}";
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..lastTrackerLibraryLocation = val),
);
}
}

View file

@ -193,5 +193,23 @@ class _TrackStateProviderElement
@override
ItemType? get itemType => (origin as TrackStateProvider).itemType;
}
String _$lastTrackerLibraryLocationStateHash() =>
r'f1e13ed88277e26123c2384fbfd4992678ad498a';
/// See also [LastTrackerLibraryLocationState].
@ProviderFor(LastTrackerLibraryLocationState)
final lastTrackerLibraryLocationStateProvider = AutoDisposeNotifierProvider<
LastTrackerLibraryLocationState, (int, bool)>.internal(
LastTrackerLibraryLocationState.new,
name: r'lastTrackerLibraryLocationStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$lastTrackerLibraryLocationStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LastTrackerLibraryLocationState = AutoDisposeNotifier<(int, bool)>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -452,7 +452,7 @@ class _RestoreKotatsuBackupProviderElement
}
String _$restoreTachiBkBackupHash() =>
r'76021dbcf0d576b50379f19f17e6a3ee8434942c';
r'd46c9a68ea7227857abd1d5ea8dce62d1ea374ca';
/// See also [restoreTachiBkBackup].
@ProviderFor(restoreTachiBkBackup)

View file

@ -25,9 +25,7 @@ final navigationItems = {
"/history": "History",
"/browse": "Browse",
"/more": "More",
"/trackerLibrary/anilist": "AL",
"/trackerLibrary/kitsu": "Kitsu",
"/trackerLibrary/mal": "MAL",
"/trackerLibrary": "Tracking",
};
class SettingsSection extends StatelessWidget {

View file

@ -157,9 +157,7 @@ class NavigationOrderState extends _$NavigationOrderState {
'/history',
'/browse',
'/more',
'/trackerLibrary/anilist',
'/trackerLibrary/kitsu',
'/trackerLibrary/mal',
'/trackerLibrary',
];
@override
@ -189,12 +187,7 @@ class NavigationOrderState extends _$NavigationOrderState {
class HideItemsState extends _$HideItemsState {
@override
List<String> build() {
return isar.settings.getSync(227)!.hideItems ??
[
'/trackerLibrary/anilist',
'/trackerLibrary/kitsu',
'/trackerLibrary/mal',
];
return isar.settings.getSync(227)!.hideItems ?? ['/trackerLibrary'];
}
void set(List<String> values) {

View file

@ -158,7 +158,7 @@ final fullScreenReaderStateProvider =
typedef _$FullScreenReaderState = AutoDisposeNotifier<bool>;
String _$navigationOrderStateHash() =>
r'f300869743afaccfd47210115f341d25fec522bb';
r'15ad6f31d8c5f3acd8cd6f4e4131e7536914cfc6';
/// See also [NavigationOrderState].
@ProviderFor(NavigationOrderState)
@ -174,7 +174,7 @@ final navigationOrderStateProvider =
);
typedef _$NavigationOrderState = AutoDisposeNotifier<List<String>>;
String _$hideItemsStateHash() => r'6844a05786f6c547a7cba261f742e82d871b6cb1';
String _$hideItemsStateHash() => r'312605bf45a83d7628d9c1da0597bb151362052b';
/// See also [HideItemsState].
@ProviderFor(HideItemsState)

View file

@ -2,13 +2,18 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_qjs/quickjs/ffi.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/l10n/generated/app_localizations.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/tracker_library/tracker_item_card.dart';
import 'package:mangayomi/modules/widgets/bottom_text_widget.dart';
@ -34,22 +39,21 @@ class TrackLibrarySection {
String name;
Future<List<TrackSearch>?> Function() func;
ItemType itemType;
int syncId;
bool isSearch;
TrackLibrarySection({
required this.name,
required this.func,
required this.syncId,
this.itemType = ItemType.manga,
this.isSearch = false,
});
}
class TrackerLibraryScreen extends ConsumerStatefulWidget {
final TrackerProviders trackerProvider;
final String? presetInput;
const TrackerLibraryScreen({
required this.trackerProvider,
required this.presetInput,
super.key,
});
const TrackerLibraryScreen({required this.presetInput, super.key});
@override
ConsumerState<TrackerLibraryScreen> createState() =>
@ -57,40 +61,139 @@ class TrackerLibraryScreen extends ConsumerStatefulWidget {
}
class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
late final _textEditingController = TextEditingController();
late String _query = "";
late bool _isSearch = false;
List<TrackLibrarySection> _sections = [];
List<TrackPreference> _preferences = [];
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context)!;
final sections = switch (widget.trackerProvider.syncId) {
1 => _sectionsMAL(),
2 => _sectionsAL(),
3 => _sectionsKitsu(),
final lastLocation = ref.watch(lastTrackerLibraryLocationStateProvider);
final trackerProvider =
TrackerProviders.values.firstWhereOrNull(
(t) => t.syncId == lastLocation.$1,
) ??
TrackerProviders.myAnimeList;
final itemType = lastLocation.$2 ? ItemType.manga : ItemType.anime;
_sections = switch (trackerProvider.syncId) {
1 => _sectionsMAL(trackerProvider.syncId, itemType),
2 => _sectionsAL(trackerProvider.syncId, itemType),
3 => _sectionsKitsu(trackerProvider.syncId, itemType),
_ => [],
};
if (_isSearch && _query.isNotEmpty) {
_sections.insert(
0,
TrackLibrarySection(
name: "Search results",
syncId: trackerProvider.syncId,
func: _fetchSearch(trackerProvider.syncId, _query, itemType),
itemType: itemType,
isSearch: true,
),
);
}
return Scaffold(
appBar: AppBar(title: Text(widget.trackerProvider.name)),
appBar: AppBar(
title: Text(
"${trackerProvider.name} | ${itemType == ItemType.anime ? l10n.anime : l10n.manga}",
),
leading: !_isSearch ? null : Container(),
actions: [
_isSearch
? SeachFormTextField(
onFieldSubmitted: (submit) {
setState(() {
if (submit.isNotEmpty) {
_query = submit;
}
});
},
onChanged: (value) {},
onSuffixPressed: () {
_query = "";
_textEditingController.clear();
setState(() {});
},
onPressed: () {
setState(() {
_isSearch = false;
_query = "";
_textEditingController.clear();
});
},
controller: _textEditingController,
)
: IconButton(
splashRadius: 20,
onPressed: () {
setState(() {
_isSearch = true;
});
},
icon: Icon(Icons.search, color: Theme.of(context).hintColor),
),
IconButton(
splashRadius: 20,
onPressed: () async => await _resetData(trackerProvider, itemType),
icon: Icon(
Icons.refresh_outlined,
color: Theme.of(context).hintColor,
),
),
IconButton(
splashRadius: 20,
onPressed: () {
_openSwitchProviderDialog(l10n);
},
icon: CircleAvatar(
radius: 14,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: trackInfos(trackerProvider.syncId).$3,
),
width: 60,
height: 70,
child: Image.asset(
trackInfos(trackerProvider.syncId).$1,
height: 30,
),
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(15),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
child: StreamBuilder(
stream: isar.trackPreferences
.filter()
.syncIdEqualTo(widget.trackerProvider.syncId)
.watch(fireImmediately: true),
stream: isar.trackPreferences.filter().syncIdIsNotNull().watch(
fireImmediately: true,
),
builder: (context, snapshot) {
List<TrackPreference> entries = snapshot.hasData
? snapshot.data ?? []
: [];
return entries.isNotEmpty
? SuperListView.builder(
itemCount: sections.length,
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
itemBuilder: (context, index) {
final section = sections[index];
return SizedBox(
height: 260,
child: TrackerSectionScreen(section: section),
);
_preferences = snapshot.hasData ? snapshot.data ?? [] : [];
return _preferences.any((p) => p.syncId == trackerProvider.syncId)
? RefreshIndicator(
onRefresh: () async {
await _resetData(trackerProvider, itemType);
},
child: SuperListView.builder(
itemCount: _sections.length,
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
itemBuilder: (context, index) {
final section = _sections[index];
return SizedBox(
height: 260,
child: TrackerSectionScreen(
key: ValueKey(section.name),
section: section,
),
);
},
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -107,192 +210,384 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
);
}
List<TrackLibrarySection> _sectionsMAL() {
return [
TrackLibrarySection(
name: "Airing Anime",
func: _fetchGeneralData(ItemType.anime),
itemType: ItemType.anime,
Future<void> _resetData(
TrackerProviders trackerProvider,
ItemType itemType,
) async {
final box = await Hive.openBox("tracker_library");
final keys = box.keys.where(
(e) => (e as String).startsWith(
"${trackerProvider.syncId}-${itemType.name}-",
),
TrackLibrarySection(
name: "Popular Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "bypopularity"),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Upcoming Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "upcoming"),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Continue watching",
func: _fetchUserData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "bypopularity"),
),
TrackLibrarySection(
name: "Top Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "manga"),
),
TrackLibrarySection(
name: "Top Manhwa",
func: _fetchGeneralData(ItemType.manga, rankingType: "manhwa"),
),
TrackLibrarySection(
name: "Top Manhua",
func: _fetchGeneralData(ItemType.manga, rankingType: "manhua"),
),
TrackLibrarySection(
name: "Continue reading",
func: _fetchUserData(ItemType.manga),
),
];
);
await box.deleteAll(keys);
setState(() {});
}
List<TrackLibrarySection> _sectionsKitsu() {
return [
TrackLibrarySection(
name: "Popular Anime",
func: _fetchGeneralData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Latest Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "-updatedAt"),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Best Rated Anime",
func: _fetchGeneralData(ItemType.anime, rankingType: "-averageRating"),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Continue watching",
func: _fetchUserData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Manga",
func: _fetchGeneralData(ItemType.manga),
),
TrackLibrarySection(
name: "Latest Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "-updatedAt"),
),
TrackLibrarySection(
name: "Best Rated Manga",
func: _fetchGeneralData(ItemType.manga, rankingType: "-averageRating"),
),
TrackLibrarySection(
name: "Continue reading",
func: _fetchUserData(ItemType.manga),
),
];
List<TrackLibrarySection> _sectionsMAL(int syncId, ItemType itemType) {
return itemType == ItemType.anime
? [
TrackLibrarySection(
name: "Airing Anime",
syncId: syncId,
func: _fetchGeneralData(syncId, ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType: "bypopularity",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Upcoming Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType: "upcoming",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Continue watching",
syncId: syncId,
func: _fetchUserData(syncId, ItemType.anime),
itemType: ItemType.anime,
),
]
: [
TrackLibrarySection(
name: "Popular Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "bypopularity",
),
),
TrackLibrarySection(
name: "Top Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "manga",
),
),
TrackLibrarySection(
name: "Top Manhwa",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "manhwa",
),
),
TrackLibrarySection(
name: "Top Manhua",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "manhua",
),
),
TrackLibrarySection(
name: "Continue reading",
syncId: syncId,
func: _fetchUserData(syncId, ItemType.manga),
),
];
}
List<TrackLibrarySection> _sectionsAL() {
return [
TrackLibrarySection(
name: "Upcoming Anime",
func: _fetchGeneralData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Anime",
func: _fetchGeneralData(
ItemType.anime,
rankingType: "sort: POPULARITY_DESC",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Trending Anime",
func: _fetchGeneralData(
ItemType.anime,
rankingType: "sort: TRENDING_DESC",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Latest Anime",
func: _fetchGeneralData(
ItemType.anime,
rankingType:
"sort: [UPDATED_AT_DESC, POPULARITY_DESC], status: RELEASING",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Continue watching",
func: _fetchUserData(ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Upcoming Manga",
func: _fetchGeneralData(ItemType.manga),
),
TrackLibrarySection(
name: "Popular Manga",
func: _fetchGeneralData(
ItemType.manga,
rankingType: "sort: POPULARITY_DESC",
),
),
TrackLibrarySection(
name: "Trending Manga",
func: _fetchGeneralData(
ItemType.manga,
rankingType: "sort: TRENDING_DESC",
),
),
TrackLibrarySection(
name: "Latest Manga",
func: _fetchGeneralData(
ItemType.manga,
rankingType:
"sort: [UPDATED_AT_DESC, POPULARITY_DESC], status: RELEASING",
),
),
TrackLibrarySection(
name: "Continue reading",
func: _fetchUserData(ItemType.manga),
),
];
List<TrackLibrarySection> _sectionsKitsu(int syncId, ItemType itemType) {
return itemType == ItemType.anime
? [
TrackLibrarySection(
name: "Popular Anime",
syncId: syncId,
func: _fetchGeneralData(syncId, ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Latest Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType: "-updatedAt",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Best Rated Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType: "-averageRating",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Continue watching",
syncId: syncId,
func: _fetchUserData(syncId, ItemType.anime),
itemType: ItemType.anime,
),
]
: [
TrackLibrarySection(
name: "Popular Manga",
syncId: syncId,
func: _fetchGeneralData(syncId, ItemType.manga),
),
TrackLibrarySection(
name: "Latest Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "-updatedAt",
),
),
TrackLibrarySection(
name: "Best Rated Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "-averageRating",
),
),
TrackLibrarySection(
name: "Continue reading",
syncId: syncId,
func: _fetchUserData(syncId, ItemType.manga),
),
];
}
List<TrackLibrarySection> _sectionsAL(int syncId, ItemType itemType) {
return itemType == ItemType.anime
? [
TrackLibrarySection(
name: "Upcoming Anime",
syncId: syncId,
func: _fetchGeneralData(syncId, ItemType.anime),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Popular Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType: "sort: POPULARITY_DESC",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Trending Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType: "sort: TRENDING_DESC",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Latest Anime",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.anime,
rankingType:
"sort: [UPDATED_AT_DESC, POPULARITY_DESC], status: RELEASING",
),
itemType: ItemType.anime,
),
TrackLibrarySection(
name: "Continue watching",
syncId: syncId,
func: _fetchUserData(syncId, ItemType.anime),
itemType: ItemType.anime,
),
]
: [
TrackLibrarySection(
name: "Upcoming Manga",
syncId: syncId,
func: _fetchGeneralData(syncId, ItemType.manga),
),
TrackLibrarySection(
name: "Popular Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "sort: POPULARITY_DESC",
),
),
TrackLibrarySection(
name: "Trending Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType: "sort: TRENDING_DESC",
),
),
TrackLibrarySection(
name: "Latest Manga",
syncId: syncId,
func: _fetchGeneralData(
syncId,
ItemType.manga,
rankingType:
"sort: [UPDATED_AT_DESC, POPULARITY_DESC], status: RELEASING",
),
),
TrackLibrarySection(
name: "Continue reading",
syncId: syncId,
func: _fetchUserData(syncId, ItemType.manga),
),
];
}
Future<List<TrackSearch>?> Function() _fetchSearch(
int syncId,
String query,
ItemType itemType,
) {
return () async => await ref
.read(
trackStateProvider(
track: Track(syncId: syncId, status: TrackStatus.completed),
itemType: itemType,
).notifier,
)
.search(query);
}
Future<List<TrackSearch>?> Function() _fetchGeneralData(
int syncId,
ItemType itemType, {
String? rankingType,
}) {
return () async => await ref
.read(
trackStateProvider(
track: Track(
syncId: widget.trackerProvider.syncId,
status: TrackStatus.completed,
),
track: Track(syncId: syncId, status: TrackStatus.completed),
itemType: itemType,
).notifier,
)
.fetchGeneralData(rankingType: rankingType);
}
Future<List<TrackSearch>?> Function() _fetchUserData(ItemType itemType) {
Future<List<TrackSearch>?> Function() _fetchUserData(
int syncId,
ItemType itemType,
) {
return () async => await ref
.read(
trackStateProvider(
track: Track(
syncId: widget.trackerProvider.syncId,
status: TrackStatus.completed,
),
track: Track(syncId: syncId, status: TrackStatus.completed),
itemType: itemType,
).notifier,
)
.fetchUserData();
}
void _openSwitchProviderDialog(AppLocalizations l10n) {
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
title: Text(l10n.track_library_switch),
content: SingleChildScrollView(
child: Column(
children: [
_getListile(l10n, TrackerProviders.myAnimeList.syncId),
_getListile(l10n, TrackerProviders.anilist.syncId),
_getListile(l10n, TrackerProviders.kitsu.syncId),
],
),
),
);
},
);
}
void _openSwitchTypeDialog(AppLocalizations l10n, int syncId) {
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
title: Text(l10n.track_library_switch),
content: SingleChildScrollView(
child: Column(
children: [
_getListile(l10n, syncId, isManga: true),
_getListile(l10n, syncId, isManga: false),
],
),
),
);
},
);
}
Widget _getListile(AppLocalizations l10n, int syncId, {bool? isManga}) {
final isLoggedIn = _preferences.any((p) => p.syncId == syncId);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
leading: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: trackInfos(syncId).$3,
),
width: 60,
height: 70,
child: isManga == null
? Image.asset(trackInfos(syncId).$1, height: 30)
: Icon(
isManga ? Icons.collections_bookmark : Icons.video_collection,
size: 30,
),
),
title: Text(
isManga == null
? trackInfos(syncId).$2
: isManga
? l10n.manga
: l10n.anime,
),
enabled: isLoggedIn,
onTap: () {
if (isManga == null) {
context.pop();
_openSwitchTypeDialog(l10n, syncId);
} else {
ref.read(lastTrackerLibraryLocationStateProvider.notifier).set((
syncId,
isManga,
));
context.pop();
}
},
),
);
}
}
class TrackerSectionScreen extends StatefulWidget {
@ -305,33 +600,20 @@ class TrackerSectionScreen extends StatefulWidget {
}
class _TrackerSectionScreenState extends State<TrackerSectionScreen> {
String _errorMessage = "";
bool _isLoading = true;
List<TrackSearch> _tracks = [];
@override
void initState() {
super.initState();
_init();
_fetchData();
}
String _errorMessage = "";
bool _isLoading = true;
List<TrackSearch> tracks = [];
_init() async {
try {
_errorMessage = "";
tracks = await widget.section.func() ?? [];
if (mounted) {
setState(() {
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
rethrow;
}
@override
void didUpdateWidget(covariant TrackerSectionScreen oldWidget) {
super.didUpdateWidget(oldWidget);
_fetchData();
}
@override
@ -352,15 +634,15 @@ class _TrackerSectionScreenState extends State<TrackerSectionScreen> {
if (_errorMessage.isNotEmpty) {
return Center(child: Text(_errorMessage));
}
if (tracks.isNotEmpty) {
if (_tracks.isNotEmpty) {
return SuperListView.builder(
extentPrecalculationPolicy:
SuperPrecalculationPolicy(),
scrollDirection: Axis.horizontal,
itemCount: tracks.length,
itemCount: _tracks.length,
itemBuilder: (context, index) {
return TrackerLibraryImageCard(
track: tracks[index],
track: _tracks[index],
itemType: widget.section.itemType,
);
},
@ -375,6 +657,39 @@ class _TrackerSectionScreenState extends State<TrackerSectionScreen> {
),
);
}
_fetchData() async {
final box = await Hive.openBox("tracker_library");
final key =
"${widget.section.syncId}-${widget.section.itemType.name}-${widget.section.name}";
if (!widget.section.isSearch && box.containsKey(key)) {
_errorMessage = "";
_tracks = box.get(key);
if (mounted) {
setState(() {
_isLoading = false;
});
}
return;
}
try {
_errorMessage = "";
_tracks = await widget.section.func() ?? [];
box.put(key, _tracks);
if (mounted) {
setState(() {
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
}
}
class TrackerLibraryImageCard extends ConsumerStatefulWidget {
@ -428,20 +743,52 @@ class _TrackerLibraryImageCardState
}
return ClipRRect(
borderRadius: BorderRadius.circular(5),
child: cachedNetworkImage(
imageUrl: toImgUrl(
hasData
? snapshot
.data!
.first
.customCoverFromTracker ??
snapshot.data!.first.imageUrl ??
""
: trackData.coverUrl ?? "",
),
width: 110,
height: 150,
fit: BoxFit.cover,
child: Stack(
children: [
cachedNetworkImage(
imageUrl: toImgUrl(
hasData
? snapshot
.data!
.first
.customCoverFromTracker ??
snapshot.data!.first.imageUrl ??
""
: trackData.coverUrl ?? "",
),
width: 110,
height: 150,
fit: BoxFit.cover,
),
Positioned(
top: 0,
right: 0,
child: Text.rich(
TextSpan(
style: TextStyle(
background: Paint()
..color = Theme.of(context)
.scaffoldBackgroundColor
.withValues(alpha: 0.75)
..strokeWidth = 20.0
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke,
),
children: [
WidgetSpan(
child: Icon(
Icons.star,
color: context.primaryColor,
),
),
TextSpan(
text: " ${trackData.score ?? "?"}",
),
],
),
),
),
],
),
);
},
@ -475,20 +822,6 @@ class _TrackerLibraryImageCardState
),
),
),
Positioned(
bottom: 0,
left: 0,
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Icon(Icons.star, color: context.primaryColor),
),
TextSpan(text: " ${trackData.score ?? "?"}"),
],
),
),
),
],
),
);

View file

@ -125,25 +125,8 @@ class RouterNotifier extends ChangeNotifier {
LibraryScreen(itemType: ItemType.novel, presetInput: id),
),
_genericRoute<String?>(
name: "trackerLibrary/anilist",
builder: (id) => TrackerLibraryScreen(
trackerProvider: TrackerProviders.anilist,
presetInput: id,
),
),
_genericRoute<String?>(
name: "trackerLibrary/kitsu",
builder: (id) => TrackerLibraryScreen(
trackerProvider: TrackerProviders.kitsu,
presetInput: id,
),
),
_genericRoute<String?>(
name: "trackerLibrary/mal",
builder: (id) => TrackerLibraryScreen(
trackerProvider: TrackerProviders.myAnimeList,
presetInput: id,
),
name: "trackerLibrary",
builder: (id) => TrackerLibraryScreen(presetInput: id),
),
_genericRoute(name: "history", child: const HistoryScreen()),
_genericRoute(name: "updates", child: const UpdatesScreen()),

View file

@ -6,7 +6,7 @@ part of 'aniskip.dart';
// RiverpodGenerator
// **************************************************************************
String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c';
String _$aniSkipHash() => r'887869b54e2e151633efd46da83bde845e14f421';
/// See also [AniSkip].
@ProviderFor(AniSkip)

View file

@ -130,6 +130,7 @@ class Anilist extends _$Anilist {
$contentUnit
description
startDate { year month day }
averageScore
}
}
}
@ -160,6 +161,9 @@ class Anilist extends _$Anilist {
).toString(),
publishingType: "",
publishingStatus: jsonRes['status'],
score: jsonRes["averageScore"] != null
? jsonRes["averageScore"] * 1.0
: 0,
),
)
.toList();
@ -291,7 +295,7 @@ class Anilist extends _$Anilist {
'''
query(\$id: Int!) {
Page {
mediaList(userId: \$id, type: $type) {
mediaList(userId: \$id, type: $type, status: CURRENT) {
id
status
scoreRaw: score(format: POINT_100)

View file

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

View file

@ -167,6 +167,9 @@ class Kitsu extends _$Kitsu {
publishingStatus: jsonRes['endDate'] == null
? "Publishing"
: "Finished",
score: jsonRes['averageRating'] is String
? double.parse(jsonRes['averageRating'])
: jsonRes['averageRating'],
),
)
.toList();

View file

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

View file

@ -129,7 +129,7 @@ class MyAnimeList extends _$MyAnimeList {
final url = Uri.parse('$baseApiUrl/$item/$id').replace(
queryParameters: {
'fields':
'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date',
'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date,mean',
},
);
@ -146,13 +146,13 @@ class MyAnimeList extends _$MyAnimeList {
publishingType: res["media_type"].toString().replaceAll("_", " "),
publishingStatus: res["status"].toString().replaceAll("_", " "),
trackingUrl: "https://myanimelist.net/$item/${res["id"]}",
score: res["mean"],
);
}
Future<List<TrackSearch>> fetchGeneralData({
bool isManga = true,
String rankingType =
"airing",
String rankingType = "airing",
}) async {
final accessToken = await _getAccessToken();
final item = isManga ? "manga" : "anime";
@ -178,7 +178,9 @@ class MyAnimeList extends _$MyAnimeList {
totalChapter: e["node"][contentUnit],
coverUrl: e["node"]["main_picture"]["large"] ?? "",
title: e["node"]["title"],
score: e["node"]["mean"],
score: e["node"]["mean"] is double
? e["node"]["mean"]
: ((e["node"]["mean"] ?? 0) as int).toDouble(),
startDate: e["node"]["start_date"] ?? "",
publishingType: e["node"]["media_type"].toString().replaceAll(
"_",
@ -223,7 +225,7 @@ class MyAnimeList extends _$MyAnimeList {
title: e["node"]["title"],
score: e["node"]["mean"] is double
? e["node"]["mean"]
: (e["node"]["mean"] as int).toDouble(),
: ((e["node"]["mean"] ?? 0) as int).toDouble(),
startDate: e["node"]["start_date"] ?? "",
publishingType: e["node"]["media_type"].toString().replaceAll(
"_",

View file

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

View file

@ -846,6 +846,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
hive_flutter:
dependency: "direct main"
description:
name: hive_flutter
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
url: "https://pub.dev"
source: hosted
version: "1.1.0"
hive_generator:
dependency: "direct dev"
description:
name: hive_generator
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
html:
dependency: "direct main"
description:
@ -1716,6 +1740,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
source_span:
dependency: transitive
description:

View file

@ -92,6 +92,8 @@ dependencies:
url: https://github.com/kodjodevf/epubx.dart.git
ref: dev
d4rt: 0.0.8
hive: ^2.2.3
hive_flutter: ^1.1.0
dependency_overrides:
ffi: ^2.1.3
@ -115,6 +117,7 @@ dev_dependencies:
freezed: ^2.0.0
inno_bundle: ^0.9.0
protoc_plugin: ^22.0.1
hive_generator: ^2.0.1
flutter:
uses-material-design: true