Merge pull request #562 from Schnitzel5/feature/watch-order

added watch order
This commit is contained in:
Moustapha Kodjo Amadou 2025-08-26 20:06:17 +01:00 committed by GitHub
commit 22a8db791b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 753 additions and 0 deletions

View file

@ -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:"
}

View file

@ -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:

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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:';
}

View file

@ -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: [

View 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;
}
}

View file

@ -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>({

View 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,
});
}