mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-19 05:42:07 +00:00
850 lines
27 KiB
Dart
850 lines
27 KiB
Dart
// ignore_for_file: use_build_context_synchronously
|
|
|
|
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';
|
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
|
import 'package:mangayomi/utils/cached_network.dart';
|
|
import 'package:mangayomi/utils/constant.dart';
|
|
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
|
|
|
enum TrackerProviders {
|
|
myAnimeList(syncId: 1, name: "MAL"),
|
|
anilist(syncId: 2, name: "AL"),
|
|
kitsu(syncId: 3, name: "Kitsu"),
|
|
trakt(syncId: 4, name: "Trakt");
|
|
|
|
const TrackerProviders({required this.syncId, required this.name});
|
|
|
|
final int syncId;
|
|
final String name;
|
|
}
|
|
|
|
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 String? presetInput;
|
|
const TrackerLibraryScreen({required this.presetInput, super.key});
|
|
|
|
@override
|
|
ConsumerState<TrackerLibraryScreen> createState() =>
|
|
_TrackerLibraryScreenState();
|
|
}
|
|
|
|
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 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(
|
|
"${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.symmetric(horizontal: 15, vertical: 5),
|
|
child: StreamBuilder(
|
|
stream: isar.trackPreferences.filter().syncIdIsNotNull().watch(
|
|
fireImmediately: true,
|
|
),
|
|
builder: (context, snapshot) {
|
|
_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,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [Text(l10n.track_library_not_logged)],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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}-",
|
|
),
|
|
);
|
|
await box.deleteAll(keys);
|
|
setState(() {});
|
|
}
|
|
|
|
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> _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: syncId, status: TrackStatus.completed),
|
|
itemType: itemType,
|
|
).notifier,
|
|
)
|
|
.fetchGeneralData(rankingType: rankingType);
|
|
}
|
|
|
|
Future<List<TrackSearch>?> Function() _fetchUserData(
|
|
int syncId,
|
|
ItemType itemType,
|
|
) {
|
|
return () async => await ref
|
|
.read(
|
|
trackStateProvider(
|
|
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 {
|
|
final TrackLibrarySection section;
|
|
|
|
const TrackerSectionScreen({super.key, required this.section});
|
|
|
|
@override
|
|
State<TrackerSectionScreen> createState() => _TrackerSectionScreenState();
|
|
}
|
|
|
|
class _TrackerSectionScreenState extends State<TrackerSectionScreen> {
|
|
String _errorMessage = "";
|
|
bool _isLoading = true;
|
|
List<TrackSearch> _tracks = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchData();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant TrackerSectionScreen oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
_fetchData();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = l10nLocalizations(context)!;
|
|
|
|
return Scaffold(
|
|
body: SizedBox(
|
|
height: 260,
|
|
child: Column(
|
|
children: [
|
|
ListTile(dense: true, title: Text(widget.section.name)),
|
|
Flexible(
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: Builder(
|
|
builder: (context) {
|
|
if (_errorMessage.isNotEmpty) {
|
|
return Center(child: Text(_errorMessage));
|
|
}
|
|
if (_tracks.isNotEmpty) {
|
|
return SuperListView.builder(
|
|
extentPrecalculationPolicy:
|
|
SuperPrecalculationPolicy(),
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: _tracks.length,
|
|
itemBuilder: (context, index) {
|
|
return TrackerLibraryImageCard(
|
|
track: _tracks[index],
|
|
itemType: widget.section.itemType,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
return Center(child: Text(l10n.no_result));
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
_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 {
|
|
final TrackSearch track;
|
|
final ItemType itemType;
|
|
|
|
const TrackerLibraryImageCard({
|
|
super.key,
|
|
required this.track,
|
|
required this.itemType,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<TrackerLibraryImageCard> createState() =>
|
|
_TrackerLibraryImageCardState();
|
|
}
|
|
|
|
class _TrackerLibraryImageCardState
|
|
extends ConsumerState<TrackerLibraryImageCard>
|
|
with AutomaticKeepAliveClientMixin<TrackerLibraryImageCard> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
final trackData = widget.track;
|
|
return GestureDetector(
|
|
onTap: () => _showCard(context),
|
|
child: StreamBuilder(
|
|
stream: isar.mangas
|
|
.filter()
|
|
.itemTypeEqualTo(widget.itemType)
|
|
.nameEqualTo(trackData.title)
|
|
.watch(fireImmediately: true),
|
|
builder: (context, snapshot) {
|
|
final hasData = snapshot.hasData && snapshot.data!.isNotEmpty;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 10),
|
|
child: Stack(
|
|
children: [
|
|
SizedBox(
|
|
width: 110,
|
|
child: Column(
|
|
children: [
|
|
Builder(
|
|
builder: (context) {
|
|
if (hasData &&
|
|
snapshot.data!.first.customCoverImage != null) {
|
|
return Image.memory(
|
|
snapshot.data!.first.customCoverImage
|
|
as Uint8List,
|
|
);
|
|
}
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(5),
|
|
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 ?? "?"}",
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
BottomTextWidget(
|
|
fontSize: 12.0,
|
|
text: trackData.title!,
|
|
isLoading: true,
|
|
textColor: Theme.of(context).textTheme.bodyLarge!.color,
|
|
isComfortableGrid: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 110,
|
|
height: 150,
|
|
color: hasData && snapshot.data!.first.favorite!
|
|
? Colors.black.withValues(alpha: 0.7)
|
|
: null,
|
|
),
|
|
if (hasData && snapshot.data!.first.favorite!)
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: Icon(
|
|
Icons.collections_bookmark,
|
|
color: context.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCard(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) =>
|
|
TrackerItemCard(track: widget.track, itemType: widget.itemType),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
class SuperPrecalculationPolicy extends ExtentPrecalculationPolicy {
|
|
@override
|
|
bool shouldPrecalculateExtents(ExtentPrecalculationContext context) {
|
|
return context.numberOfItems < 100;
|
|
}
|
|
}
|