add manga reader detail view
This commit is contained in:
parent
2b05a3ab52
commit
f9976f0148
17 changed files with 731 additions and 68 deletions
4
lib/constant.dart
Normal file
4
lib/constant.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class HiveConstant {
|
||||
static String get hiveBoxManga => "manga_box";
|
||||
static String get hiveBoxMangaInfo => "manga_box_info";
|
||||
}
|
||||
|
|
@ -1,8 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mangayomi/constant.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/router/router.dart';
|
||||
|
||||
void main() {
|
||||
void main() async {
|
||||
await Hive.initFlutter();
|
||||
Hive.registerAdapter(ModelMangaAdapter());
|
||||
await Hive.openBox<ModelManga>(
|
||||
HiveConstant.hiveBoxManga,
|
||||
); await Hive.openBox(
|
||||
HiveConstant.hiveBoxMangaInfo,
|
||||
);
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
|
|
|
|||
12
lib/providers/hive_provider.dart
Normal file
12
lib/providers/hive_provider.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mangayomi/constant.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
|
||||
final hiveBoxManga = Provider<Box<ModelManga>>((ref) {
|
||||
return Hive.box<ModelManga>(HiveConstant.hiveBoxManga);
|
||||
});
|
||||
|
||||
final hiveBoxMangaInfo = Provider<Box>((ref) {
|
||||
return Hive.box(HiveConstant.hiveBoxMangaInfo);
|
||||
});
|
||||
|
|
@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mangayomi/models/manga_type.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/views/browse/browse_screen.dart';
|
||||
import 'package:mangayomi/views/general/general_screen.dart';
|
||||
import 'package:mangayomi/views/history/history_screen.dart';
|
||||
import 'package:mangayomi/views/library/library_screen.dart';
|
||||
import 'package:mangayomi/views/manga/detail/manga_reader_detail.dart';
|
||||
import 'package:mangayomi/views/manga/home/home.dart';
|
||||
import 'package:mangayomi/views/more/more_screen.dart';
|
||||
import 'package:mangayomi/views/updates/updates_screen.dart';
|
||||
|
|
@ -90,6 +92,28 @@ class AsyncRouterNotifier extends ChangeNotifier {
|
|||
),
|
||||
);
|
||||
}),
|
||||
GoRoute(
|
||||
path: '/manga-reader/detail',
|
||||
builder: (context, state) {
|
||||
ModelManga? model;
|
||||
|
||||
model = state.extra as ModelManga;
|
||||
|
||||
return MangaReaderDetail(
|
||||
modelManga: model,
|
||||
);
|
||||
},
|
||||
pageBuilder: (context, state) {
|
||||
ModelManga? model;
|
||||
|
||||
model = state.extra as ModelManga;
|
||||
|
||||
return CustomTransition(
|
||||
key: state.pageKey,
|
||||
child: MangaReaderDetail(
|
||||
modelManga: model,
|
||||
));
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'dart:developer';
|
||||
import 'package:mangayomi/services/http_res_to_dom_html.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'get_manga_detail.g.dart';
|
||||
|
||||
class GetMangaDetailModel {
|
||||
List<String> genre = [];
|
||||
List<String> detail = [];
|
||||
List<String> chapterTitle = [];
|
||||
List<String> chapterUrl = [];
|
||||
List<String> chapterDate = [];
|
||||
String? author;
|
||||
String? status;
|
||||
String? source;
|
||||
String? url;
|
||||
String? name;
|
||||
String? image;
|
||||
String? synopsys;
|
||||
String? imageUrl;
|
||||
String? description;
|
||||
GetMangaDetailModel({
|
||||
required this.genre,
|
||||
required this.detail,
|
||||
required this.author,
|
||||
required this.status,
|
||||
required this.chapterDate,
|
||||
required this.chapterTitle,
|
||||
required this.chapterUrl,
|
||||
required this.image,
|
||||
required this.synopsys,
|
||||
required this.imageUrl,
|
||||
required this.description,
|
||||
required this.url,
|
||||
required this.name,
|
||||
required this.source,
|
||||
|
|
@ -31,69 +33,71 @@ class GetMangaDetailModel {
|
|||
|
||||
@riverpod
|
||||
Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
||||
{required String image,
|
||||
{required String imageUrl,
|
||||
required String url,
|
||||
required String name,
|
||||
String lang = '',
|
||||
required String lang,
|
||||
required String source}) async {
|
||||
List<String> genre = [];
|
||||
List<String> detail = [];
|
||||
String? author;
|
||||
String? status;
|
||||
List<String> chapterTitle = [];
|
||||
List<String> chapterUrl = [];
|
||||
List<String> chapterDate = [];
|
||||
String? synopsys;
|
||||
source = source.toLowerCase();
|
||||
String? description;
|
||||
if (source == "mangahere") {
|
||||
final response = await http.get(Uri.parse("http://www.mangahere.cc$url"),
|
||||
final dom = await httpResToDom(
|
||||
url: "http://www.mangahere.cc$url",
|
||||
headers: {
|
||||
"Referer": "https://www.mangahere.cc/",
|
||||
"Cookie": "isAdult=1"
|
||||
});
|
||||
dom.Document htmll = dom.Document.html(response.body);
|
||||
|
||||
if (htmll
|
||||
if (dom
|
||||
.querySelectorAll(
|
||||
' body > div > div > div.detail-info-right > p.detail-info-right-title > span.detail-info-right-title-tip')
|
||||
.isNotEmpty) {
|
||||
final tt = htmll
|
||||
final tt = dom
|
||||
.querySelectorAll(
|
||||
' body > div > div > div.detail-info-right > p.detail-info-right-title > span.detail-info-right-title-tip')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
|
||||
detail.add(tt[0]);
|
||||
status = tt[0];
|
||||
} else {
|
||||
detail.add("");
|
||||
status = "";
|
||||
}
|
||||
if (htmll
|
||||
if (dom
|
||||
.querySelectorAll(
|
||||
' body > div > div > div.detail-info-right > p.detail-info-right-say > a')
|
||||
.isNotEmpty) {
|
||||
final tt = htmll
|
||||
final tt = dom
|
||||
.querySelectorAll(
|
||||
' body > div > div > div.detail-info-right > p.detail-info-right-say > a')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
|
||||
detail.add(tt[0]);
|
||||
author = tt[0];
|
||||
} else {
|
||||
detail.add("");
|
||||
author = "";
|
||||
}
|
||||
|
||||
if (htmll
|
||||
if (dom
|
||||
.querySelectorAll(
|
||||
'body > div > div > div.detail-info-right > p.detail-info-right-content')
|
||||
.isNotEmpty) {
|
||||
final tt = htmll
|
||||
final tt = dom
|
||||
.querySelectorAll(
|
||||
'body > div > div > div.detail-info-right > p.detail-info-right-content')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
|
||||
synopsys = tt.first;
|
||||
description = tt.first;
|
||||
}
|
||||
|
||||
if (htmll.querySelectorAll('ul > li > a').isNotEmpty) {
|
||||
final udl = htmll
|
||||
if (dom.querySelectorAll('ul > li > a').isNotEmpty) {
|
||||
final udl = dom
|
||||
.querySelectorAll('ul > li > a ')
|
||||
.where((e) => e.attributes.containsKey('href'))
|
||||
.map((e) => e.attributes['href'])
|
||||
|
|
@ -103,8 +107,8 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
|||
chapterUrl.add(ok!);
|
||||
}
|
||||
}
|
||||
if (htmll.querySelectorAll('ul > li > a > div > p.title3').isNotEmpty) {
|
||||
final tt = htmll
|
||||
if (dom.querySelectorAll('ul > li > a > div > p.title3').isNotEmpty) {
|
||||
final tt = dom
|
||||
.querySelectorAll('ul > li > a > div > p.title3')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
|
|
@ -112,8 +116,8 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
|||
chapterTitle.add(ok);
|
||||
}
|
||||
}
|
||||
if (htmll.querySelectorAll('ul > li > a > div > p.title2').isNotEmpty) {
|
||||
final tt = htmll
|
||||
if (dom.querySelectorAll('ul > li > a > div > p.title2').isNotEmpty) {
|
||||
final tt = dom
|
||||
.querySelectorAll('ul > li > a > div > p.title2')
|
||||
.map((e) => e.text.trim())
|
||||
.toList();
|
||||
|
|
@ -121,11 +125,11 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
|||
chapterDate.add(ok);
|
||||
}
|
||||
}
|
||||
if (htmll
|
||||
if (dom
|
||||
.querySelectorAll(
|
||||
' body > div > div > div.detail-info-right > p.detail-info-right-tag-list > a')
|
||||
.isNotEmpty) {
|
||||
final tt = htmll
|
||||
final tt = dom
|
||||
.querySelectorAll(
|
||||
' body > div > div > div.detail-info-right > p.detail-info-right-tag-list > a')
|
||||
.map((e) => e.text.trim())
|
||||
|
|
@ -136,15 +140,18 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetMangaDetailModel(
|
||||
chapterDate: chapterDate,
|
||||
chapterTitle: chapterTitle,
|
||||
chapterUrl: chapterUrl,
|
||||
detail: detail,
|
||||
genre: genre,
|
||||
image: image,
|
||||
synopsys: synopsys,
|
||||
name: name,
|
||||
url: url,
|
||||
source: source);
|
||||
chapterDate: chapterDate,
|
||||
chapterTitle: chapterTitle,
|
||||
chapterUrl: chapterUrl,
|
||||
status: status,
|
||||
genre: genre,
|
||||
author: author,
|
||||
description: description,
|
||||
name: name,
|
||||
url: url,
|
||||
source: source,
|
||||
imageUrl: imageUrl,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_manga_detail.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getMangaDetailHash() => r'f8a4047fc7d1661b6abdfcfa0786e0d78a20c8ed';
|
||||
String _$getMangaDetailHash() => r'17d72a984e6428e71778ade3650b56062ee779d7';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
@ -42,14 +42,14 @@ class GetMangaDetailFamily extends Family<AsyncValue<GetMangaDetailModel>> {
|
|||
|
||||
/// See also [getMangaDetail].
|
||||
GetMangaDetailProvider call({
|
||||
required String image,
|
||||
required String imageUrl,
|
||||
required String url,
|
||||
required String name,
|
||||
String lang = '',
|
||||
required String lang,
|
||||
required String source,
|
||||
}) {
|
||||
return GetMangaDetailProvider(
|
||||
image: image,
|
||||
imageUrl: imageUrl,
|
||||
url: url,
|
||||
name: name,
|
||||
lang: lang,
|
||||
|
|
@ -62,7 +62,7 @@ class GetMangaDetailFamily extends Family<AsyncValue<GetMangaDetailModel>> {
|
|||
covariant GetMangaDetailProvider provider,
|
||||
) {
|
||||
return call(
|
||||
image: provider.image,
|
||||
imageUrl: provider.imageUrl,
|
||||
url: provider.url,
|
||||
name: provider.name,
|
||||
lang: provider.lang,
|
||||
|
|
@ -90,15 +90,15 @@ class GetMangaDetailProvider
|
|||
extends AutoDisposeFutureProvider<GetMangaDetailModel> {
|
||||
/// See also [getMangaDetail].
|
||||
GetMangaDetailProvider({
|
||||
required this.image,
|
||||
required this.imageUrl,
|
||||
required this.url,
|
||||
required this.name,
|
||||
this.lang = '',
|
||||
required this.lang,
|
||||
required this.source,
|
||||
}) : super.internal(
|
||||
(ref) => getMangaDetail(
|
||||
ref,
|
||||
image: image,
|
||||
imageUrl: imageUrl,
|
||||
url: url,
|
||||
name: name,
|
||||
lang: lang,
|
||||
|
|
@ -115,7 +115,7 @@ class GetMangaDetailProvider
|
|||
GetMangaDetailFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String image;
|
||||
final String imageUrl;
|
||||
final String url;
|
||||
final String name;
|
||||
final String lang;
|
||||
|
|
@ -124,7 +124,7 @@ class GetMangaDetailProvider
|
|||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is GetMangaDetailProvider &&
|
||||
other.image == image &&
|
||||
other.imageUrl == imageUrl &&
|
||||
other.url == url &&
|
||||
other.name == name &&
|
||||
other.lang == lang &&
|
||||
|
|
@ -134,7 +134,7 @@ class GetMangaDetailProvider
|
|||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, image.hashCode);
|
||||
hash = _SystemHash.combine(hash, imageUrl.hashCode);
|
||||
hash = _SystemHash.combine(hash, url.hashCode);
|
||||
hash = _SystemHash.combine(hash, name.hashCode);
|
||||
hash = _SystemHash.combine(hash, lang.hashCode);
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ Future<GetMangaModel> getPopularManga(GetPopularMangaRef ref,
|
|||
List<String?> url = [];
|
||||
List<String?> name = [];
|
||||
List<String?> image = [];
|
||||
|
||||
source = source.toLowerCase();
|
||||
//mangahere
|
||||
if (source == "mangahere") {
|
||||
final dom = await httpResToDom(url: 'https://www.mangahere.cc/ranking/');
|
||||
final dom = await httpResToDom(
|
||||
url: 'https://www.mangahere.cc/ranking/', headers: {});
|
||||
if (dom
|
||||
.querySelectorAll(
|
||||
'body > div.container.weekrank.ranking > div > div > ul > li > a')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_popular_manga.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getPopularMangaHash() => r'4ab45f760fe457710bb9d09c72faef2fc09261e2';
|
||||
String _$getPopularMangaHash() => r'0b6445ff81dbbe1d337f37ed46544f5834805088';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
Future<Document> httpResToDom({required String url}) async {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
Future<Document> httpResToDom(
|
||||
{required String url, required Map<String, String>? headers}) async {
|
||||
final response = await http.get(Uri.parse(url), headers: headers);
|
||||
return Document.html(response.body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class _SourcesScreenState extends State<SourcesScreen> {
|
|||
onTap: () {
|
||||
context.push('/mangaHome',
|
||||
extra: MangaType(
|
||||
isFullData: true, lang: 'en', source: 'mangahere'));
|
||||
isFullData: true, lang: 'en', source: 'MangaHere'));
|
||||
},
|
||||
leading: Container(
|
||||
height: 37,
|
||||
|
|
|
|||
371
lib/views/manga/detail/card_sliver_app_bar.dart
Normal file
371
lib/views/manga/detail/card_sliver_app_bar.dart
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
import 'package:blur/blur.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:draggable_home/draggable_home.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/utils/media_query.dart';
|
||||
import 'package:mangayomi/views/manga/detail/readmore.dart';
|
||||
|
||||
class CardSliverAppBar extends ConsumerStatefulWidget {
|
||||
final double? height;
|
||||
final Function(bool) isExtended;
|
||||
final double? appBarHeight = 65;
|
||||
final Text? title;
|
||||
final Widget? titleDescription;
|
||||
final List<Color>? backButtonColors;
|
||||
final Widget? action;
|
||||
final bool? isManga;
|
||||
final String? description;
|
||||
final ModelManga? modelManga;
|
||||
final List<String>? genre;
|
||||
final List<String>? chapterTitle;
|
||||
const CardSliverAppBar({
|
||||
super.key,
|
||||
required this.isExtended,
|
||||
required this.isManga,
|
||||
required this.height,
|
||||
required this.title,
|
||||
this.titleDescription,
|
||||
this.backButtonColors,
|
||||
this.action,
|
||||
required this.description,
|
||||
required this.genre,
|
||||
required this.chapterTitle,
|
||||
required this.modelManga,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<CardSliverAppBar> createState() => _CardSliverAppBarState();
|
||||
}
|
||||
|
||||
class _CardSliverAppBarState extends ConsumerState<CardSliverAppBar> {
|
||||
bool _reverse = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NotificationListener<UserScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.direction == ScrollDirection.forward) {
|
||||
widget.isExtended(true);
|
||||
}
|
||||
if (notification.direction == ScrollDirection.reverse) {
|
||||
widget.isExtended(false);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: DraggableHome(
|
||||
centerTitle: false,
|
||||
leading: const BackButton(),
|
||||
headerExpandedHeight: mediaHeight(context, 1) <= 600 ? 0.50 : 0.35,
|
||||
title: Text(widget.modelManga!.name!),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: _reverse
|
||||
? const Icon(FontAwesomeIcons.arrowDownShortWide)
|
||||
: const Icon(FontAwesomeIcons.arrowUpShortWide),
|
||||
iconSize: 25,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reverse = !_reverse;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
headerWidget: Stack(
|
||||
children: [
|
||||
_backgroundConstructor(),
|
||||
SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
_titleConstructor(),
|
||||
_cardConstructor(),
|
||||
if (widget.action != null) _actionConstructor(),
|
||||
_backButtonConstructor(),
|
||||
_filterConstructor(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: [
|
||||
_bodyContainer(),
|
||||
_listView(),
|
||||
const SizedBox(
|
||||
height: 50,
|
||||
)
|
||||
],
|
||||
fullyStretchable: false,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBarColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ListView _listView() {
|
||||
return ListView.builder(
|
||||
// controller: _scrollController,
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
reverse: _reverse,
|
||||
itemCount: widget.modelManga!.chapterTitle!.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
key: ObjectKey(widget.modelManga!.chapterUrl),
|
||||
onTap: () {},
|
||||
trailing: const Icon(FontAwesomeIcons.circleDown),
|
||||
subtitle: widget.isManga == true
|
||||
? Text(
|
||||
widget.modelManga!.chapterDate![index],
|
||||
style: const TextStyle(fontSize: 13),
|
||||
)
|
||||
: const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
title: Text(
|
||||
widget.modelManga!.chapterTitle![index],
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _backButtonConstructor() {
|
||||
return Positioned(
|
||||
top: 7,
|
||||
left: 5,
|
||||
child: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
iconSize: 25,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _filterConstructor() {
|
||||
return Positioned(
|
||||
top: 7,
|
||||
right: 0,
|
||||
child: IconButton(
|
||||
icon: _reverse
|
||||
? const Icon(FontAwesomeIcons.arrowDownShortWide)
|
||||
: const Icon(FontAwesomeIcons.arrowUpShortWide),
|
||||
iconSize: 25,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reverse = !_reverse;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bodyContainer() {
|
||||
return Container(
|
||||
key: const Key("widget_body"),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Wrap(
|
||||
children: [
|
||||
for (var i = 0; i < widget.genre!.length; i++)
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 2, right: 2, bottom: 5),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(width: 1, color: Colors.white),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2, horizontal: 8),
|
||||
child: Text(
|
||||
widget.genre![i],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
// log
|
||||
Column(
|
||||
children: [
|
||||
//Description
|
||||
if (widget.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ReadMoreWidget(
|
||||
text: widget.description!,
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
'${widget.chapterTitle!.length.toString()} chapter(s)',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// _body
|
||||
// ,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cardConstructor() {
|
||||
return Positioned(
|
||||
key: const Key("widget_card"),
|
||||
top: widget.height! - (widget.appBarHeight! * 1.8),
|
||||
left: 20,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: SizedBox(
|
||||
width: widget.appBarHeight! * 1.5,
|
||||
height: widget.appBarHeight! * 2.3,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
image: DecorationImage(
|
||||
image: CachedNetworkImageProvider(widget.modelManga!.imageUrl!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _backgroundConstructor() {
|
||||
return SizedBox(
|
||||
key: const Key("widget_background"),
|
||||
height: widget.height! + 400,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
// color: Colors.black,
|
||||
child: Blur(
|
||||
blur: 10,
|
||||
blurColor: Theme.of(context).primaryColor,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
image: DecorationImage(
|
||||
image: CachedNetworkImageProvider(widget.modelManga!.imageUrl!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _titleConstructor() {
|
||||
return Positioned(
|
||||
key: const Key("widget_title"),
|
||||
top: widget.height! - widget.appBarHeight!,
|
||||
left: 30,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(left: 100),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.3),
|
||||
height: 70,
|
||||
child: _titleDescriptionHandler(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _titleDescriptionHandler() {
|
||||
var titleContainer = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 50,
|
||||
),
|
||||
child: widget.title,
|
||||
);
|
||||
|
||||
var titleDescriptionContainer = Opacity(
|
||||
opacity: 1.0,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 25,
|
||||
),
|
||||
child: widget.titleDescription,
|
||||
),
|
||||
);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
titleContainer,
|
||||
titleDescriptionContainer,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionConstructor() {
|
||||
return Positioned(
|
||||
key: const Key("widget_action"),
|
||||
top: widget.height! - widget.appBarHeight! - 45,
|
||||
right: 10,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 3.0)
|
||||
]),
|
||||
child: widget.action,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 3.0)
|
||||
]),
|
||||
child: IconButton(
|
||||
onPressed: () async {},
|
||||
icon: const Icon(Icons.travel_explore)),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
110
lib/views/manga/detail/manga_reader_detail.dart
Normal file
110
lib/views/manga/detail/manga_reader_detail.dart
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/providers/hive_provider.dart';
|
||||
import 'package:mangayomi/services/get_manga_detail.dart';
|
||||
import 'package:mangayomi/views/manga/detail/manga_details_view.dart';
|
||||
|
||||
class MangaReaderDetail extends ConsumerStatefulWidget {
|
||||
final ModelManga modelManga;
|
||||
const MangaReaderDetail({super.key, required this.modelManga});
|
||||
|
||||
@override
|
||||
ConsumerState<MangaReaderDetail> createState() => _MangaReaderDetailState();
|
||||
}
|
||||
|
||||
class _MangaReaderDetailState extends ConsumerState<MangaReaderDetail> {
|
||||
bool _isFavorite = false;
|
||||
@override
|
||||
void initState() {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (_isFavorite) {
|
||||
bool isOk = false;
|
||||
ref
|
||||
.watch(getMangaDetailProvider(
|
||||
imageUrl: '',
|
||||
lang: widget.modelManga.lang!,
|
||||
name: widget.modelManga.name!,
|
||||
source: widget.modelManga.source!,
|
||||
url: widget.modelManga.link!)
|
||||
.future)
|
||||
.then((value) {
|
||||
final model = ModelManga(
|
||||
imageUrl: widget.modelManga.imageUrl,
|
||||
name: widget.modelManga.name,
|
||||
genre: widget.modelManga.genre,
|
||||
author: widget.modelManga.author,
|
||||
chapterDate: value.chapterDate,
|
||||
chapterTitle: value.chapterTitle,
|
||||
chapterUrl: value.chapterUrl,
|
||||
description: widget.modelManga.description,
|
||||
status: value.status,
|
||||
favorite: _isFavorite,
|
||||
link: widget.modelManga.link,
|
||||
source: widget.modelManga.source,
|
||||
lang: widget.modelManga.lang);
|
||||
ref.watch(hiveBoxManga).put(widget.modelManga.link!, model);
|
||||
setState(() {
|
||||
isOk = true;
|
||||
});
|
||||
});
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (isOk == true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: ValueListenableBuilder<Box<ModelManga>>(
|
||||
valueListenable: ref.watch(hiveBoxManga).listenable(),
|
||||
builder: (context, value, child) {
|
||||
final entries = value.values
|
||||
.where((element) => element.link == widget.modelManga.link)
|
||||
.toList();
|
||||
if (entries.isNotEmpty) {
|
||||
return MangaDetailsView(
|
||||
modelManga: entries[0],
|
||||
isFavorite: (value) {
|
||||
setState(() {
|
||||
_isFavorite = value;
|
||||
});
|
||||
},
|
||||
isManga: true,
|
||||
);
|
||||
}
|
||||
return MangaDetailsView(
|
||||
modelManga: widget.modelManga,
|
||||
isFavorite: (value) {
|
||||
setState(() {
|
||||
_isFavorite = value;
|
||||
});
|
||||
},
|
||||
isManga: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/views/manga/detail/readmore.dart
Normal file
67
lib/views/manga/detail/readmore.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:expandable_text/expandable_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mangayomi/utils/media_query.dart';
|
||||
|
||||
class ReadMoreWidget extends StatefulWidget {
|
||||
const ReadMoreWidget({Key? key, required this.text}) : super(key: key);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
ReadMoreWidgetState createState() => ReadMoreWidgetState();
|
||||
}
|
||||
|
||||
class ReadMoreWidgetState extends State<ReadMoreWidget>
|
||||
with TickerProviderStateMixin {
|
||||
bool expanded = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ExpandableText(
|
||||
animationDuration: const Duration(milliseconds: 500),
|
||||
onExpandedChanged: (ok) {
|
||||
setState(() => expanded = ok);
|
||||
},
|
||||
expandOnTextTap: true,
|
||||
widget.text,
|
||||
expandText: '',
|
||||
maxLines: 3,
|
||||
expanded: false,
|
||||
onPrefixTap: () {
|
||||
setState(() => expanded = !expanded);
|
||||
},
|
||||
linkColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
animation: true,
|
||||
collapseOnTextTap: true,
|
||||
prefixText: '',
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).cardColor.withOpacity(0.15),
|
||||
width: mediaWidth(context, 1),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
expanded
|
||||
? Row(
|
||||
children: const [
|
||||
Text(
|
||||
"Show less",
|
||||
),
|
||||
Icon(Icons.keyboard_arrow_up_sharp)
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: const [
|
||||
Text(
|
||||
"Show more",
|
||||
),
|
||||
Icon(Icons.keyboard_arrow_down_sharp)
|
||||
],
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -150,11 +150,11 @@ class _MangaHomeImageCardState extends ConsumerState<MangaHomeImageCard>
|
|||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final getMangaDetail = ref.watch(getMangaDetailProvider(
|
||||
source: widget.source,
|
||||
image: widget.image,
|
||||
name: widget.name,
|
||||
url: widget.url,
|
||||
));
|
||||
source: widget.source,
|
||||
imageUrl: widget.image,
|
||||
name: widget.name,
|
||||
url: widget.url,
|
||||
lang: widget.lang));
|
||||
|
||||
return getMangaDetail.when(
|
||||
data: (data) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mangayomi/models/model_manga.dart';
|
||||
import 'package:mangayomi/services/get_manga_detail.dart';
|
||||
import 'package:mangayomi/utils/cached_network.dart';
|
||||
import 'package:mangayomi/views/widgets/bottom_text_widget.dart';
|
||||
|
|
@ -27,14 +29,32 @@ class _MangaImageCardWidgetState extends ConsumerState<MangaImageCardWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {},
|
||||
onTap: () async {
|
||||
final modelManga = ModelManga(
|
||||
imageUrl: widget.getMangaDetailModel!.imageUrl,
|
||||
name: widget.getMangaDetailModel!.name,
|
||||
genre: widget.getMangaDetailModel!.genre,
|
||||
author: widget.getMangaDetailModel!.author,
|
||||
chapterDate: widget.getMangaDetailModel!.chapterDate,
|
||||
chapterTitle: widget.getMangaDetailModel!.chapterTitle,
|
||||
chapterUrl: widget.getMangaDetailModel!.chapterUrl,
|
||||
status: widget.getMangaDetailModel!.status,
|
||||
description: widget.getMangaDetailModel!.description,
|
||||
favorite: false,
|
||||
link: widget.getMangaDetailModel!.url,
|
||||
source: widget.getMangaDetailModel!.source,
|
||||
lang: widget.lang);
|
||||
if (mounted) {
|
||||
context.push('/manga-reader/detail', extra: modelManga);
|
||||
}
|
||||
},
|
||||
child: CoverViewWidget(children: [
|
||||
cachedNetworkImage(
|
||||
headers: {
|
||||
"Referer": "https://www.mangahere.cc/",
|
||||
"Cookie": "isAdult=1"
|
||||
},
|
||||
imageUrl: widget.getMangaDetailModel!.image!,
|
||||
imageUrl: widget.getMangaDetailModel!.imageUrl!,
|
||||
width: 200,
|
||||
height: 270,
|
||||
fit: BoxFit.cover),
|
||||
|
|
|
|||
32
pubspec.lock
32
pubspec.lock
|
|
@ -41,6 +41,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
blur:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: blur
|
||||
sha256: fd23f1247faee4a7d1a3efb6b7c3cea134f3b939d72e5f8d45233deb0776259f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -241,6 +249,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
draggable_home:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: draggable_home
|
||||
sha256: "4fa3c71f7784bb9a0ab722bb6245e071c261dcf2a3e1e004ee681bda60cfbbbb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
expandable_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: expandable_text
|
||||
sha256: "7d03ea48af6987b20ece232678b744862aa3250d4a71e2aaf1e4af90015d76b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -328,6 +352,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
font_awesome_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.0"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ dependencies:
|
|||
riverpod_annotation: ^2.0.2
|
||||
html: ^0.15.2
|
||||
flutter_js: ^0.6.0
|
||||
font_awesome_flutter: ^10.1.0
|
||||
blur: ^3.1.0
|
||||
draggable_home: ^1.0.4
|
||||
expandable_text: ^2.3.0
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
|
|
|||
Loading…
Reference in a new issue