mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
added watch order
This commit is contained in:
parent
6101d96c96
commit
b57015b682
20 changed files with 446 additions and 0 deletions
|
|
@ -536,6 +536,7 @@
|
|||
"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",
|
||||
"recommendations": "Recommendations",
|
||||
"recommendations_similarity": "Similarity:"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3291,6 +3291,12 @@ 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 @recommendations_similarity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1703,6 +1703,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1705,6 +1705,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1716,6 +1716,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1704,6 +1704,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1721,6 +1721,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1722,6 +1722,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1706,6 +1706,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1710,6 +1710,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1719,6 +1719,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1718,6 +1718,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1720,6 +1720,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1704,6 +1704,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1710,6 +1710,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1675,6 +1675,9 @@ 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 recommendations_similarity => 'Similarity:';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1643,6 +1643,32 @@ 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,
|
||||
);
|
||||
},
|
||||
label: Text(l10n.watch_order),
|
||||
icon: Icon(Icons.arrow_right_alt_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
if (!context.isTablet)
|
||||
Column(
|
||||
children: [
|
||||
|
|
|
|||
250
lib/modules/manga/detail/widgets/watch_order_screen.dart
Normal file
250
lib/modules/manga/detail/widgets/watch_order_screen.dart
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mangayomi/models/manga.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: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;
|
||||
|
||||
const WatchOrderScreen({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<WatchOrderScreen> createState() => _WatchOrderScreenState();
|
||||
}
|
||||
|
||||
class _WatchOrderScreenState extends State<WatchOrderScreen> {
|
||||
String _errorMessage = "";
|
||||
bool _isLoading = true;
|
||||
List<WatchOrderSearch>? dataSearch;
|
||||
List<WatchOrderItem>? data;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
_errorMessage = "";
|
||||
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(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));
|
||||
}
|
||||
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(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) {
|
||||
final imageProvider = CustomExtendedNetworkImageProvider(
|
||||
toImgUrl(imageUrl ?? ""),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_openImage(context, imageProvider);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
height: 150,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
image: DecorationImage(image: imageProvider, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,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 +257,10 @@ class RouterNotifier extends ChangeNotifier {
|
|||
algorithmWeights: data.$3,
|
||||
),
|
||||
),
|
||||
_genericRoute<String>(
|
||||
name: "watchOrder",
|
||||
builder: (data) => WatchOrderScreen(name: data),
|
||||
),
|
||||
];
|
||||
|
||||
GoRoute _genericRoute<T>({
|
||||
|
|
|
|||
116
lib/services/fetch_watch_order.dart
Normal file
116
lib/services/fetch_watch_order.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
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';
|
||||
|
||||
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 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