mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-20 19:12:04 +00:00
Merge pull request #562 from Schnitzel5/feature/watch-order
added watch order
This commit is contained in:
commit
22a8db791b
20 changed files with 753 additions and 0 deletions
|
|
@ -537,6 +537,8 @@
|
|||
"clear_library": "Clear library",
|
||||
"clear_library_desc": "Choose to clear all manga, anime and/or novel entries",
|
||||
"clear_library_input": "Type 'manga', 'anime' and/or 'novel' (separated by a comma) to remove all related entries",
|
||||
"watch_order": "Watch order",
|
||||
"sequels": "Sequels",
|
||||
"recommendations": "Recommendations",
|
||||
"recommendations_similarity": "Similarity:"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3297,6 +3297,18 @@ abstract class AppLocalizations {
|
|||
/// **'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries'**
|
||||
String get clear_library_input;
|
||||
|
||||
/// No description provided for @watch_order.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Watch order'**
|
||||
String get watch_order;
|
||||
|
||||
/// No description provided for @sequels.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sequels'**
|
||||
String get sequels;
|
||||
|
||||
/// No description provided for @recommendations_similarity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1706,6 +1706,12 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1708,6 +1708,12 @@ class AppLocalizationsAs extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1719,6 +1719,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1707,6 +1707,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1724,6 +1724,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1725,6 +1725,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1709,6 +1709,12 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1713,6 +1713,12 @@ class AppLocalizationsId extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1722,6 +1722,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1721,6 +1721,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1723,6 +1723,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1707,6 +1707,12 @@ class AppLocalizationsTh extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1713,6 +1713,12 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1678,6 +1678,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
String get clear_library_input =>
|
||||
'Type \'manga\', \'anime\' and/or \'novel\' (separated by a comma) to remove all related entries';
|
||||
|
||||
@override
|
||||
String get watch_order => 'Watch order';
|
||||
|
||||
@override
|
||||
String get sequels => 'Sequels';
|
||||
|
||||
@override
|
||||
String get recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provi
|
|||
import 'package:mangayomi/modules/more/providers/algorithm_weights_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
|
||||
import 'package:mangayomi/modules/tracker_library/tracker_library_screen.dart';
|
||||
import 'package:mangayomi/modules/widgets/bottom_select_bar.dart';
|
||||
import 'package:mangayomi/modules/widgets/category_selection_dialog.dart';
|
||||
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
|
||||
|
|
@ -1643,6 +1644,86 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
if (widget.manga!.itemType == ItemType.anime)
|
||||
SizedBox(
|
||||
width: context.width(1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: OutlinedButton.icon(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.push(
|
||||
"/watchOrder",
|
||||
extra: (widget.manga!.name, null),
|
||||
);
|
||||
},
|
||||
label: Text(l10n.watch_order),
|
||||
icon: Icon(Icons.arrow_right_alt_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.manga!.itemType == ItemType.anime)
|
||||
StreamBuilder(
|
||||
stream: isar.tracks
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.mangaIdEqualTo(widget.manga!.id!)
|
||||
.watch(fireImmediately: true),
|
||||
builder: (context, snapshot) {
|
||||
List<Track>? trackRes = snapshot.hasData
|
||||
? snapshot.data
|
||||
: [];
|
||||
final isNotSupported =
|
||||
trackRes?.firstOrNull?.syncId !=
|
||||
TrackerProviders.myAnimeList.syncId &&
|
||||
trackRes?.firstOrNull?.syncId !=
|
||||
TrackerProviders.anilist.syncId;
|
||||
if ((trackRes?.isEmpty ?? true) || (isNotSupported)) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 15),
|
||||
SizedBox(
|
||||
width: context.width(1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: OutlinedButton.icon(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
30.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.push(
|
||||
"/watchOrder",
|
||||
extra: (
|
||||
widget.manga!.name,
|
||||
trackRes?.firstOrNull,
|
||||
),
|
||||
);
|
||||
},
|
||||
label: Text(l10n.sequels),
|
||||
icon: Icon(Icons.arrow_right_alt_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
if (!context.isTablet)
|
||||
Column(
|
||||
children: [
|
||||
|
|
|
|||
350
lib/modules/manga/detail/widgets/watch_order_screen.dart
Normal file
350
lib/modules/manga/detail/widgets/watch_order_screen.dart
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:isar/isar.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/modules/tracker_library/tracker_library_screen.dart';
|
||||
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/services/fetch_watch_order.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
class WatchOrderScreen extends StatefulWidget {
|
||||
final String name;
|
||||
final Track? track;
|
||||
|
||||
const WatchOrderScreen({super.key, required this.name, required this.track});
|
||||
|
||||
@override
|
||||
State<WatchOrderScreen> createState() => _WatchOrderScreenState();
|
||||
}
|
||||
|
||||
class _WatchOrderScreenState extends State<WatchOrderScreen> {
|
||||
String _errorMessage = "";
|
||||
bool _isLoading = true;
|
||||
List<SequelItem>? sequels;
|
||||
List<WatchOrderSearch>? dataSearch;
|
||||
List<WatchOrderItem>? data;
|
||||
|
||||
bool get isSequels => widget.track != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
_errorMessage = "";
|
||||
if (isSequels) {
|
||||
final mediaId = widget.track!.mediaId!.toString();
|
||||
final mal = await isar.trackPreferences
|
||||
.filter()
|
||||
.syncIdEqualTo(TrackerProviders.myAnimeList.syncId)
|
||||
.findFirst();
|
||||
final anilist = await isar.trackPreferences
|
||||
.filter()
|
||||
.syncIdEqualTo(TrackerProviders.anilist.syncId)
|
||||
.findFirst();
|
||||
final data = await fetchSequels(mal?.username, anilist?.username);
|
||||
sequels = data
|
||||
.where((e) => e.reason.any((r) => r.id == mediaId))
|
||||
.toList();
|
||||
} else {
|
||||
dataSearch = await searchWatchOrder(widget.name);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(isSequels ? l10n.sequels : l10n.watch_order)),
|
||||
body: Padding(
|
||||
padding: EdgeInsetsGeometry.all(5),
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Builder(
|
||||
builder: (context) {
|
||||
if (_errorMessage.isNotEmpty) {
|
||||
return Center(child: Text(_errorMessage));
|
||||
}
|
||||
return isSequels ? _buildSequels() : _buildWatchOrder();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSequels() {
|
||||
if (sequels != null && sequels!.isNotEmpty) {
|
||||
return SuperListView.builder(
|
||||
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
|
||||
itemCount: sequels!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sequel = sequels![index];
|
||||
return StreamBuilder(
|
||||
stream: isar.tracks
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.mediaIdEqualTo(int.tryParse(sequel.id))
|
||||
.or()
|
||||
.mediaIdEqualTo(int.tryParse(sequel.anilistId ?? ""))
|
||||
.watch(fireImmediately: true),
|
||||
builder: (context, snapshot) {
|
||||
final hasData = snapshot.hasData && snapshot.data!.isNotEmpty;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
context.push(
|
||||
'/globalSearch',
|
||||
extra: (sequel.title, ItemType.anime),
|
||||
);
|
||||
},
|
||||
title: Row(
|
||||
children: [
|
||||
_thumbnailPreview(context, sequel.image, hasData: hasData),
|
||||
const SizedBox(width: 15),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(sequel.title, context),
|
||||
Text(
|
||||
"${sequel.period} | ${sequel.type} | ${sequel.episodes} episodes | ★${sequel.score} (${sequel.scoreUsers})",
|
||||
style: const TextStyle(fontSize: 11),
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Center(child: Text(context.l10n.no_result));
|
||||
}
|
||||
|
||||
Widget _buildWatchOrder() {
|
||||
final isSearch = dataSearch != null && dataSearch!.isNotEmpty;
|
||||
final isWatchOrder = data != null && data!.isNotEmpty;
|
||||
if (isSearch || isWatchOrder) {
|
||||
return SuperListView.builder(
|
||||
extentPrecalculationPolicy: SuperPrecalculationPolicy(),
|
||||
itemCount: data?.length ?? dataSearch!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final search = !isWatchOrder && isSearch ? dataSearch![index] : null;
|
||||
final watchOrder = isWatchOrder ? data![index] : null;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
if (isWatchOrder) {
|
||||
context.push(
|
||||
'/globalSearch',
|
||||
extra: (
|
||||
watchOrder!.nameEnglish ?? watchOrder.name,
|
||||
ItemType.anime,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = "";
|
||||
});
|
||||
data = await fetchWatchOrder(search!.id);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
title: Row(
|
||||
children: [
|
||||
_thumbnailPreview(context, watchOrder?.image ?? search!.image),
|
||||
const SizedBox(width: 15),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(watchOrder?.name ?? search!.name, context),
|
||||
if (watchOrder?.nameEnglish != null &&
|
||||
watchOrder?.nameEnglish != watchOrder?.text)
|
||||
Text(
|
||||
watchOrder!.nameEnglish!,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
Text(
|
||||
watchOrder?.text ?? "${search!.type} - ${search.year}",
|
||||
style: const TextStyle(fontSize: 11),
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Center(child: Text(context.l10n.no_result));
|
||||
}
|
||||
|
||||
Widget _buildTitle(String text, BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Make sure that (constraints.maxWidth - (35 + 5)) is strictly positive.
|
||||
final double availableWidth = constraints.maxWidth - (35 + 5);
|
||||
final textPainter =
|
||||
TextPainter(
|
||||
text: TextSpan(text: text, style: const TextStyle(fontSize: 13)),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(
|
||||
maxWidth: availableWidth > 0 ? availableWidth : 1.0,
|
||||
); // - Download icon size (download_page_widget.dart, Widget Build SizedBox width: 35)
|
||||
|
||||
final isOverflowing = textPainter.didExceedMaxLines;
|
||||
|
||||
if (isOverflowing) {
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Marquee(
|
||||
text: text,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
|
||||
blankSpace: 40.0,
|
||||
velocity: 30.0,
|
||||
pauseAfterRound: const Duration(seconds: 1),
|
||||
startPadding: 10.0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _thumbnailPreview(
|
||||
BuildContext context,
|
||||
String? imageUrl, {
|
||||
bool hasData = false,
|
||||
}) {
|
||||
final imageProvider = CustomExtendedNetworkImageProvider(
|
||||
toImgUrl(imageUrl ?? ""),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_openImage(context, imageProvider);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 150,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
image: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: hasData ? Colors.black.withValues(alpha: 0.7) : null,
|
||||
),
|
||||
if (hasData)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.collections_bookmark,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openImage(BuildContext context, ImageProvider imageProvider) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: PhotoViewGallery.builder(
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
itemCount: 1,
|
||||
builder: (context, index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: imageProvider,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: 2.0,
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, event) {
|
||||
return const ProgressCenter();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuperPrecalculationPolicy extends ExtentPrecalculationPolicy {
|
||||
@override
|
||||
bool shouldPrecalculateExtents(ExtentPrecalculationContext context) {
|
||||
return context.numberOfItems < 100;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.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:mangayomi/models/track_search.dart';
|
||||
import 'package:mangayomi/modules/anime/anime_player_view.dart';
|
||||
|
|
@ -15,6 +16,7 @@ import 'package:mangayomi/modules/browse/sources/sources_filter_screen.dart';
|
|||
import 'package:mangayomi/modules/calendar/calendar_screen.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/migrate_screen.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/recommendation_screen.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/watch_order_screen.dart';
|
||||
import 'package:mangayomi/modules/more/data_and_storage/create_backup.dart';
|
||||
import 'package:mangayomi/modules/more/data_and_storage/data_and_storage.dart';
|
||||
import 'package:mangayomi/modules/more/settings/appearance/custom_navigation_settings.dart';
|
||||
|
|
@ -256,6 +258,10 @@ class RouterNotifier extends ChangeNotifier {
|
|||
algorithmWeights: data.$3,
|
||||
),
|
||||
),
|
||||
_genericRoute<(String, Track?)>(
|
||||
name: "watchOrder",
|
||||
builder: (data) => WatchOrderScreen(name: data.$1, track: data.$2),
|
||||
),
|
||||
];
|
||||
|
||||
GoRoute _genericRoute<T>({
|
||||
|
|
|
|||
218
lib/services/fetch_watch_order.dart
Normal file
218
lib/services/fetch_watch_order.dart
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
|
||||
|
||||
const _sequelData =
|
||||
"&types%5B%5D=1&types%5B%5D=3&types%5B%5D=2&types%5B%5D=4&types%5B%5D=9&score=0&date_from=false&date_to=false&include_ptw=1&exclude_h=1&exclude_planned=1&exclude_dropped=0&exclude_not_aired=0&exclude_short=1&exclude_short_value=3";
|
||||
|
||||
Future<List<SequelItem>> fetchSequels(
|
||||
String? malUsername,
|
||||
String? anilistUsername,
|
||||
) async {
|
||||
if (malUsername == null && anilistUsername == null) {
|
||||
return [];
|
||||
}
|
||||
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
|
||||
try {
|
||||
final url = Uri.parse("https://chiaki.site/?/tools/sequel_locator_fetch");
|
||||
final res = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"priority": "u=1, i",
|
||||
"Referer": "https://chiaki.site/?/tools/watch_order",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
},
|
||||
body:
|
||||
"user=${malUsername ?? anilistUsername}&list_source=${malUsername != null ? "mal" : "anilist"}$_sequelData",
|
||||
);
|
||||
final data = jsonDecode(res.body) as Map<String, dynamic>?;
|
||||
return (data?["data"] as List?)
|
||||
?.map((e) => SequelItem.fromJson(e))
|
||||
.toList() ??
|
||||
[];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<WatchOrderSearch>> searchWatchOrder(String name) async {
|
||||
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
|
||||
try {
|
||||
final url = Uri.parse(
|
||||
"https://chiaki.site/?/tools/autocomplete_series&term=$name",
|
||||
);
|
||||
final res = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
"priority": "u=1, i",
|
||||
"Referer": "https://chiaki.site/?/tools/watch_order",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
},
|
||||
);
|
||||
final data = jsonDecode(res.body) as List?;
|
||||
return data?.map((e) => WatchOrderSearch.fromJson(e)).toList() ?? [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<WatchOrderItem>> fetchWatchOrder(String id) async {
|
||||
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
|
||||
try {
|
||||
final res = await http.get(
|
||||
Uri.parse("https://chiaki.site/?/tools/watch_order/id/$id"),
|
||||
headers: {
|
||||
"priority": "u=1, i",
|
||||
"Referer": "https://chiaki.site/?/tools/watch_order",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
},
|
||||
);
|
||||
final doc = Document.html(res.body);
|
||||
return doc
|
||||
.select("table > tbody > tr")
|
||||
?.map((e) {
|
||||
final img = e.selectFirst("td > div.wo_avatar_big")?.outerHtml;
|
||||
final startIdx = img?.indexOf("url('") ?? -1;
|
||||
final endIdx = img?.indexOf("')", max(0, startIdx)) ?? -1;
|
||||
return WatchOrderItem(
|
||||
id: e.attr("data-id") ?? id,
|
||||
anilistId: e.attr("data-anilist-id") ?? "",
|
||||
image: startIdx != -1 && endIdx != -1
|
||||
? "https://chiaki.site/${img?.substring(startIdx + 5, endIdx)}"
|
||||
: "",
|
||||
name:
|
||||
e.selectFirst("td > span.wo_title")?.text ??
|
||||
"Unknown title",
|
||||
nameEnglish: e.selectFirst("td > span.uk-text-small")?.text,
|
||||
text:
|
||||
e
|
||||
.selectFirst("td > span.uk-text-muted.uk-text-small")
|
||||
?.text ??
|
||||
"",
|
||||
);
|
||||
})
|
||||
.where((e) => e.name != "Unknown title")
|
||||
.toList() ??
|
||||
[];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class SequelItem {
|
||||
final String id;
|
||||
final String? anilistId;
|
||||
final String image;
|
||||
final String episodes;
|
||||
final String title;
|
||||
final String group;
|
||||
final String groupId;
|
||||
final String period;
|
||||
final String score;
|
||||
final String scoreUsers;
|
||||
final String type;
|
||||
final List<SequelReason> reason;
|
||||
|
||||
SequelItem({
|
||||
required this.id,
|
||||
required this.anilistId,
|
||||
required this.image,
|
||||
required this.episodes,
|
||||
required this.title,
|
||||
required this.group,
|
||||
required this.groupId,
|
||||
required this.period,
|
||||
required this.score,
|
||||
required this.scoreUsers,
|
||||
required this.type,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
factory SequelItem.fromJson(Map<String, dynamic> json) {
|
||||
return SequelItem(
|
||||
id: json["id"],
|
||||
anilistId: json["anilist_id"],
|
||||
image: "https://chiaki.site/${json["image_url"]}",
|
||||
episodes: json["episodes"],
|
||||
title: json["title"],
|
||||
group: json["group"],
|
||||
groupId: json["group_id"],
|
||||
period: json["period"],
|
||||
score: json["score"],
|
||||
scoreUsers: json["score_users"],
|
||||
type: json["type"],
|
||||
reason:
|
||||
(json["reason"] as List?)
|
||||
?.map((e) => SequelReason.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SequelReason {
|
||||
final String id;
|
||||
final String image;
|
||||
final String title;
|
||||
|
||||
SequelReason({required this.id, required this.image, required this.title});
|
||||
|
||||
factory SequelReason.fromJson(Map<String, dynamic> json) {
|
||||
return SequelReason(
|
||||
id: json["id"],
|
||||
image: "https://chiaki.site/${json["image_url"]}",
|
||||
title: json["title"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WatchOrderSearch {
|
||||
final String id;
|
||||
final String image;
|
||||
final String type;
|
||||
final String name;
|
||||
final int year;
|
||||
|
||||
WatchOrderSearch({
|
||||
required this.id,
|
||||
required this.image,
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.year,
|
||||
});
|
||||
|
||||
factory WatchOrderSearch.fromJson(Map<String, dynamic> json) {
|
||||
return WatchOrderSearch(
|
||||
id: json["id"],
|
||||
image: "https://chiaki.site/${json["image"]}",
|
||||
type: json["type"],
|
||||
name: json["value"],
|
||||
year: json["year"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WatchOrderItem {
|
||||
final String id;
|
||||
final String anilistId;
|
||||
final String image;
|
||||
final String name;
|
||||
final String? nameEnglish;
|
||||
final String text;
|
||||
|
||||
WatchOrderItem({
|
||||
required this.id,
|
||||
required this.anilistId,
|
||||
required this.image,
|
||||
required this.name,
|
||||
required this.nameEnglish,
|
||||
required this.text,
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue