mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: pgc review
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
Binary file not shown.
@@ -13,13 +13,15 @@ class CustomIcon {
|
||||
static const IconData share_line = _CustomIconData(0xe807);
|
||||
static const IconData share_node = _CustomIconData(0xe808);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe809);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80a);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe80e);
|
||||
static const IconData topic_tag = _CustomIconData(0xe80f);
|
||||
static const IconData watch_later = _CustomIconData(0xe810);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80a);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe810);
|
||||
static const IconData topic_tag = _CustomIconData(0xe811);
|
||||
static const IconData watch_later = _CustomIconData(0xe812);
|
||||
}
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
|
||||
@@ -855,4 +855,14 @@ class Api {
|
||||
static const String delFavTopic = '/x/topic/fav/sub/cancel';
|
||||
|
||||
static const String likeTopic = '/x/topic/like';
|
||||
|
||||
static const String pgcReviewL = '/pgc/review/long/list';
|
||||
|
||||
static const String pgcReviewS = '/pgc/review/short/list';
|
||||
|
||||
static const String pgcReviewLike = '/pgc/review/action/like';
|
||||
|
||||
static const String pgcReviewDislike = '/pgc/review/action/dislike';
|
||||
|
||||
static const String pgcReviewPost = '/pgc/review/short/post';
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/bangumi/list.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_index/condition.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/data.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_timeline/pgc_timeline.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_timeline/result.dart';
|
||||
import 'package:PiliPlus/models/common/pgc_review_type.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart' show Accounts;
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class BangumiHttp {
|
||||
static Future<LoadingState> pgcIndexResult({
|
||||
@@ -107,4 +111,93 @@ class BangumiHttp {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<PgcReviewData>> pgcReview({
|
||||
required PgcReviewType type,
|
||||
required mediaId,
|
||||
int sort = 0,
|
||||
String? next,
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
type.api,
|
||||
queryParameters: {
|
||||
'media_id': mediaId,
|
||||
'ps': 20,
|
||||
'sort': sort,
|
||||
if (next != null) 'cursor': next,
|
||||
'web_location': 666.19,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(PgcReviewData.fromJson(res.data['data']));
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future pgcReviewLike({
|
||||
required mediaId,
|
||||
required reviewId,
|
||||
}) async {
|
||||
var res = await Request().post(
|
||||
Api.pgcReviewLike,
|
||||
data: {
|
||||
'media_id': mediaId,
|
||||
'review_type': 2,
|
||||
'review_id': reviewId,
|
||||
'csrf': Accounts.main.csrf,
|
||||
},
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
static Future pgcReviewDislike({
|
||||
required mediaId,
|
||||
required reviewId,
|
||||
}) async {
|
||||
var res = await Request().post(
|
||||
Api.pgcReviewDislike,
|
||||
data: {
|
||||
'media_id': mediaId,
|
||||
'review_type': 2,
|
||||
'review_id': reviewId,
|
||||
'csrf': Accounts.main.csrf,
|
||||
},
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
static Future pgcReviewPost({
|
||||
required mediaId,
|
||||
required int score,
|
||||
required String content,
|
||||
bool shareFeed = false,
|
||||
}) async {
|
||||
var res = await Request().post(
|
||||
Api.pgcReviewPost,
|
||||
data: {
|
||||
'media_id': mediaId,
|
||||
'score': score,
|
||||
'content': content,
|
||||
if (shareFeed) 'share_feed': 1,
|
||||
'csrf': Accounts.main.csrf,
|
||||
},
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
lib/models/bangumi/pgc_review/author.dart
Normal file
27
lib/models/bangumi/pgc_review/author.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:PiliPlus/models/model_avatar.dart' show Vip;
|
||||
|
||||
class Author {
|
||||
String? avatar;
|
||||
int? level;
|
||||
int? mid;
|
||||
String? uname;
|
||||
Vip? vip;
|
||||
|
||||
Author({
|
||||
this.avatar,
|
||||
this.level,
|
||||
this.mid,
|
||||
this.uname,
|
||||
this.vip,
|
||||
});
|
||||
|
||||
factory Author.fromJson(Map<String, dynamic> json) => Author(
|
||||
avatar: json['avatar'] as String?,
|
||||
level: json['level'] as int?,
|
||||
mid: json['mid'] as int?,
|
||||
uname: json['uname'] as String?,
|
||||
vip: json['vip'] == null
|
||||
? null
|
||||
: Vip.fromJson(json['vip'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
17
lib/models/bangumi/pgc_review/data.dart
Normal file
17
lib/models/bangumi/pgc_review/data.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/list.dart';
|
||||
|
||||
class PgcReviewData {
|
||||
List<PgcReviewItemModel>? list;
|
||||
String? next;
|
||||
int? count;
|
||||
|
||||
PgcReviewData({this.list, this.next, this.count});
|
||||
|
||||
factory PgcReviewData.fromJson(Map<String, dynamic> json) => PgcReviewData(
|
||||
list: (json['list'] as List<dynamic>?)
|
||||
?.map((e) => PgcReviewItemModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
next: json['next'] as String?,
|
||||
count: json['count'] as int?,
|
||||
);
|
||||
}
|
||||
55
lib/models/bangumi/pgc_review/list.dart
Normal file
55
lib/models/bangumi/pgc_review/list.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/author.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/stat.dart';
|
||||
|
||||
class PgcReviewItemModel {
|
||||
Author? author;
|
||||
String? title;
|
||||
String? content;
|
||||
int? ctime;
|
||||
int? mediaId;
|
||||
int? mid;
|
||||
int? mtime;
|
||||
String? progress;
|
||||
String? pushTimeStr;
|
||||
int? reviewId;
|
||||
late int score;
|
||||
Stat? stat;
|
||||
int? articleId;
|
||||
|
||||
PgcReviewItemModel({
|
||||
this.author,
|
||||
this.title,
|
||||
this.content,
|
||||
this.ctime,
|
||||
this.mediaId,
|
||||
this.mid,
|
||||
this.mtime,
|
||||
this.progress,
|
||||
this.pushTimeStr,
|
||||
this.reviewId,
|
||||
required this.score,
|
||||
this.stat,
|
||||
this.articleId,
|
||||
});
|
||||
|
||||
factory PgcReviewItemModel.fromJson(Map<String, dynamic> json) =>
|
||||
PgcReviewItemModel(
|
||||
articleId: json['article_id'],
|
||||
author: json['author'] == null
|
||||
? null
|
||||
: Author.fromJson(json['author'] as Map<String, dynamic>),
|
||||
title: json['title'] as String?,
|
||||
content: json['content'] as String?,
|
||||
ctime: json['ctime'] as int?,
|
||||
mediaId: json['media_id'] as int?,
|
||||
mid: json['mid'] as int?,
|
||||
mtime: json['mtime'] as int?,
|
||||
progress: json['progress'] as String?,
|
||||
pushTimeStr: json['push_time_str'] as String?,
|
||||
reviewId: json['review_id'] as int?,
|
||||
score: json['score'] == null ? 0 : json['score'] ~/ 2,
|
||||
stat: json['stat'] == null
|
||||
? null
|
||||
: Stat.fromJson(json['stat'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
19
lib/models/bangumi/pgc_review/stat.dart
Normal file
19
lib/models/bangumi/pgc_review/stat.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class Stat {
|
||||
int? disliked;
|
||||
int? liked;
|
||||
int? likes;
|
||||
|
||||
Stat({this.disliked, this.liked, this.likes});
|
||||
|
||||
factory Stat.fromJson(Map<String, dynamic> json) => Stat(
|
||||
disliked: json['disliked'] as int?,
|
||||
liked: json['liked'] as int?,
|
||||
likes: json['likes'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'disliked': disliked,
|
||||
'liked': liked,
|
||||
'likes': likes,
|
||||
};
|
||||
}
|
||||
13
lib/models/common/pgc_review_type.dart
Normal file
13
lib/models/common/pgc_review_type.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:PiliPlus/http/api.dart';
|
||||
|
||||
enum PgcReviewType {
|
||||
long(label: '长评', api: Api.pgcReviewL),
|
||||
short(label: '短评', api: Api.pgcReviewS);
|
||||
|
||||
final String label;
|
||||
final String api;
|
||||
const PgcReviewType({
|
||||
required this.label,
|
||||
required this.api,
|
||||
});
|
||||
}
|
||||
89
lib/pages/pgc_review/child/controller.dart
Normal file
89
lib/pages/pgc_review/child/controller.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:PiliPlus/http/bangumi.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/data.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/list.dart';
|
||||
import 'package:PiliPlus/models/common/pgc_review_type.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
|
||||
class PgcReviewController
|
||||
extends CommonListController<PgcReviewData, PgcReviewItemModel> {
|
||||
PgcReviewController({required this.type, required this.mediaId});
|
||||
|
||||
final PgcReviewType type;
|
||||
final dynamic mediaId;
|
||||
|
||||
int? count;
|
||||
String? next;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
queryData();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onRefresh() {
|
||||
count = null;
|
||||
next = null;
|
||||
return super.onRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void checkIsEnd(int length) {
|
||||
if (count != null && length >= count!) {
|
||||
isEnd = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<PgcReviewItemModel>? getDataList(PgcReviewData response) {
|
||||
count = response.count;
|
||||
next = response.next;
|
||||
return response.list;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<PgcReviewData>> customGetData() => BangumiHttp.pgcReview(
|
||||
type: type,
|
||||
mediaId: mediaId,
|
||||
next: next,
|
||||
);
|
||||
|
||||
Future<void> onLike(int index, bool isLike, reviewId) async {
|
||||
var res = await BangumiHttp.pgcReviewLike(
|
||||
mediaId: mediaId,
|
||||
reviewId: reviewId,
|
||||
);
|
||||
if (res['status']) {
|
||||
final item = loadingState.value.data![index];
|
||||
int likes = item.stat?.likes ?? 0;
|
||||
item.stat
|
||||
?..liked = isLike ? 0 : 1
|
||||
..likes = isLike ? likes - 1 : likes + 1;
|
||||
if (!isLike) {
|
||||
item.stat?.disliked = 0;
|
||||
}
|
||||
loadingState.refresh();
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onDislike(int index, bool isDislike, reviewId) async {
|
||||
var res = await BangumiHttp.pgcReviewDislike(
|
||||
mediaId: mediaId,
|
||||
reviewId: reviewId,
|
||||
);
|
||||
if (res['status']) {
|
||||
final item = loadingState.value.data![index];
|
||||
item.stat?.disliked = isDislike ? 0 : 1;
|
||||
if (!isDislike) {
|
||||
item.stat?.liked = 0;
|
||||
}
|
||||
loadingState.refresh();
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
298
lib/pages/pgc_review/child/view.dart
Normal file
298
lib/pages/pgc_review/child/view.dart
Normal file
@@ -0,0 +1,298 @@
|
||||
import 'package:PiliPlus/common/skeleton/video_reply.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/bangumi/pgc_review/list.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/models/common/pgc_review_type.dart';
|
||||
import 'package:PiliPlus/pages/pgc_review/child/controller.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PgcReviewChildPage extends StatefulWidget {
|
||||
const PgcReviewChildPage({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.mediaId,
|
||||
});
|
||||
|
||||
final PgcReviewType type;
|
||||
final dynamic mediaId;
|
||||
|
||||
@override
|
||||
State<PgcReviewChildPage> createState() => _PgcReviewChildPageState();
|
||||
}
|
||||
|
||||
class _PgcReviewChildPageState extends State<PgcReviewChildPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late final _controller = Get.put(
|
||||
PgcReviewController(type: widget.type, mediaId: widget.mediaId),
|
||||
tag: '${widget.mediaId}${widget.type.name}',
|
||||
);
|
||||
late final isLongReview = widget.type == PgcReviewType.long;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Get.delete<PgcReviewController>(
|
||||
tag: '${widget.mediaId}${widget.type.name}');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final theme = Theme.of(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80),
|
||||
sliver:
|
||||
Obx(() => _buildBody(theme, _controller.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(
|
||||
ThemeData theme, LoadingState<List<PgcReviewItemModel>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => SliverToBoxAdapter(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
},
|
||||
itemCount: 8,
|
||||
),
|
||||
),
|
||||
Success(:var response) => response?.isNotEmpty == true
|
||||
? SliverList.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == response.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return _itemWidget(theme, index, response[index]);
|
||||
},
|
||||
itemCount: response!.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
)
|
||||
: HttpError(onReload: _controller.onReload),
|
||||
Error(:var errMsg) => HttpError(
|
||||
errMsg: errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Widget _itemWidget(ThemeData theme, int index, PgcReviewItemModel item) {
|
||||
return InkWell(
|
||||
onTap: isLongReview
|
||||
? () => Get.toNamed(
|
||||
'/articlePage',
|
||||
parameters: {
|
||||
'id': item.articleId!.toString(),
|
||||
'type': 'read',
|
||||
},
|
||||
)
|
||||
: null,
|
||||
onLongPress: isLongReview
|
||||
? null
|
||||
: () => showConfirmDialog(
|
||||
context: context,
|
||||
title: '确定举报该点评?',
|
||||
onConfirm: () => Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://www.bilibili.com/appeal/?reviewId=${item.reviewId}&type=shortComment&mediaId=${widget.mediaId}'
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Get.toNamed('/member?mid=${item.author!.mid}'),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
height: 34,
|
||||
width: 34,
|
||||
src: item.author!.avatar,
|
||||
type: ImageType.avatar,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
spacing: 2,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.author!.uname!,
|
||||
style: TextStyle(
|
||||
color: item.author?.vip?.status != null &&
|
||||
item.author!.vip!.status > 0 &&
|
||||
item.author!.vip!.type == 2
|
||||
? context.vipColor
|
||||
: theme.colorScheme.outline,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${item.author!.level}.png',
|
||||
height: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (item.pushTimeStr != null) ...[
|
||||
Text(
|
||||
item.pushTimeStr!,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
...List.generate(
|
||||
5,
|
||||
(index) {
|
||||
if (index <= item.score - 1) {
|
||||
return const Icon(
|
||||
CustomIcon.star_favorite_solid,
|
||||
size: 13,
|
||||
color: Color(0xFFFFAD35),
|
||||
);
|
||||
}
|
||||
return const Icon(
|
||||
CustomIcon.star_favorite_line,
|
||||
size: 14,
|
||||
color: Colors.grey,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
if (item.title != null)
|
||||
Text(
|
||||
item.title!,
|
||||
style: const TextStyle(
|
||||
height: 1.75,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (isLongReview)
|
||||
Text(
|
||||
item.content!,
|
||||
style: const TextStyle(height: 1.75),
|
||||
)
|
||||
else
|
||||
SelectableText(
|
||||
item.content!,
|
||||
style: const TextStyle(height: 1.75),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final Color color = theme.colorScheme.outline;
|
||||
final Color primary = theme.colorScheme.primary;
|
||||
final ButtonStyle style = TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
final isLike = item.stat?.liked == 1;
|
||||
late final isDislike = item.stat?.disliked == 1;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (!isLongReview)
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
style: style,
|
||||
onPressed: () => _controller.onDislike(
|
||||
index, isDislike, item.reviewId),
|
||||
child: Icon(
|
||||
isDislike
|
||||
? FontAwesomeIcons.solidThumbsDown
|
||||
: FontAwesomeIcons.thumbsDown,
|
||||
size: 16,
|
||||
color: isDislike ? primary : color,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
style: style,
|
||||
onPressed: isLongReview
|
||||
? null
|
||||
: () => _controller.onLike(
|
||||
index, isLike, item.reviewId),
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Icon(
|
||||
isLike
|
||||
? FontAwesomeIcons.solidThumbsUp
|
||||
: FontAwesomeIcons.thumbsUp,
|
||||
size: 16,
|
||||
color: isLike ? primary : color,
|
||||
),
|
||||
Text(
|
||||
Utils.numFormat(item.stat?.likes ?? 0),
|
||||
style: TextStyle(
|
||||
color: isLike ? primary : color,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
221
lib/pages/pgc_review/post/view.dart
Normal file
221
lib/pages/pgc_review/post/view.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/http/bangumi.dart';
|
||||
import 'package:PiliPlus/pages/common/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart' show Accounts;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PgcReviewPostPanel extends CommonCollapseSlidePage {
|
||||
const PgcReviewPostPanel({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.mediaId,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final dynamic mediaId;
|
||||
|
||||
@override
|
||||
State<PgcReviewPostPanel> createState() => _PgcReviewPostPanelState();
|
||||
}
|
||||
|
||||
class _PgcReviewPostPanelState
|
||||
extends CommonCollapseSlidePageState<PgcReviewPostPanel> {
|
||||
final _controller = TextEditingController();
|
||||
final RxInt _score = 0.obs;
|
||||
final RxBool _shareFeed = false.obs;
|
||||
final RxBool _enablePost = false.obs;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 16,
|
||||
toolbarHeight: 45,
|
||||
title: Text(widget.name),
|
||||
actions: [
|
||||
iconButton(
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: Get.back,
|
||||
iconSize: 22,
|
||||
bgColor: Colors.transparent,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) {
|
||||
return Obx(
|
||||
() => GestureDetector(
|
||||
onTap: () {
|
||||
_enablePost.value = true;
|
||||
_score.value = index + 1;
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: index <= _score.value - 1
|
||||
? const Icon(
|
||||
CustomIcon.star_favorite_solid,
|
||||
size: 50,
|
||||
color: Color(0xFFFFAD35),
|
||||
)
|
||||
: const Icon(
|
||||
CustomIcon.star_favorite_line,
|
||||
size: 50,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(
|
||||
() => Text(
|
||||
switch (_score.value) {
|
||||
1 => '很差',
|
||||
2 => '较差',
|
||||
3 => '还行',
|
||||
4 => '很好',
|
||||
5 => '佳作',
|
||||
_ => '轻触评分',
|
||||
},
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: _score.value == 0
|
||||
? theme.colorScheme.outline
|
||||
: const Color(0xFFFFAD35),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: TextField(
|
||||
maxLength: 100,
|
||||
minLines: 5,
|
||||
maxLines: 5,
|
||||
controller: _controller,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _shareFeed.value = !_shareFeed.value,
|
||||
child: Obx(
|
||||
() {
|
||||
Color color = _shareFeed.value
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outline;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 22,
|
||||
_shareFeed.value
|
||||
? Icons.check_box_outlined
|
||||
: Icons.check_box_outline_blank_outlined,
|
||||
color: color,
|
||||
),
|
||||
Text(
|
||||
' 分享到动态',
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 6,
|
||||
bottom: MediaQuery.paddingOf(context).bottom +
|
||||
MediaQuery.viewInsetsOf(context).bottom +
|
||||
6,
|
||||
),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 0.5,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Obx(
|
||||
() => FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
),
|
||||
onPressed: _enablePost.value
|
||||
? () async {
|
||||
if (!Accounts.main.isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
var res = await BangumiHttp.pgcReviewPost(
|
||||
mediaId: widget.mediaId,
|
||||
score: _score.value * 2,
|
||||
content: _controller.text,
|
||||
shareFeed: _shareFeed.value,
|
||||
);
|
||||
if (res['status']) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('点评成功');
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('发布'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
160
lib/pages/pgc_review/view.dart
Normal file
160
lib/pages/pgc_review/view.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'package:PiliPlus/models/common/pgc_review_type.dart';
|
||||
import 'package:PiliPlus/pages/pgc_review/child/controller.dart';
|
||||
import 'package:PiliPlus/pages/pgc_review/child/view.dart';
|
||||
import 'package:PiliPlus/pages/pgc_review/post/view.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PgcReviewPage extends StatefulWidget {
|
||||
const PgcReviewPage({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.mediaId,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final dynamic mediaId;
|
||||
|
||||
@override
|
||||
State<PgcReviewPage> createState() => _PgcReviewPageState();
|
||||
}
|
||||
|
||||
class _PgcReviewPageState extends State<PgcReviewPage>
|
||||
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late final _tabController =
|
||||
TabController(length: PgcReviewType.values.length, vsync: this);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final theme = Theme.of(context);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
dividerHeight: 0,
|
||||
indicatorWeight: 0,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
indicatorPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
|
||||
indicator: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
labelColor: theme.colorScheme.onSecondaryContainer,
|
||||
unselectedLabelColor: theme.colorScheme.outline,
|
||||
labelStyle: TabBarTheme.of(context)
|
||||
.labelStyle
|
||||
?.copyWith(fontSize: 13) ??
|
||||
const TextStyle(fontSize: 13),
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: PgcReviewType.values
|
||||
.map((e) => Tab(text: e.label))
|
||||
.toList(),
|
||||
onTap: (index) {
|
||||
try {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
final item = PgcReviewType.values[index];
|
||||
Get.find<PgcReviewController>(
|
||||
tag: '${widget.mediaId}${item.name}')
|
||||
.scrollController
|
||||
.animToTop();
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: PgcReviewType.values
|
||||
.map((e) =>
|
||||
PgcReviewChildPage(type: e, mediaId: widget.mediaId))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 16,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'写短评',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return PgcReviewPostPanel(
|
||||
name: widget.name,
|
||||
mediaId: widget.mediaId,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'写长评',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () => Get
|
||||
..back()
|
||||
..toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://member.bilibili.com/article-text/mobile?theme=${Get.isDarkMode ? 1 : 0}&media_id=${widget.mediaId}'
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.edit),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/models/bangumi/info.dart';
|
||||
import 'package:PiliPlus/pages/common/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/pgc_review/view.dart';
|
||||
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,6 +16,7 @@ class IntroDetail extends CommonCollapseSlidePage {
|
||||
const IntroDetail({
|
||||
super.key,
|
||||
required this.bangumiDetail,
|
||||
super.enableSlide = false,
|
||||
this.videoTags,
|
||||
});
|
||||
|
||||
@@ -21,32 +25,54 @@ class IntroDetail extends CommonCollapseSlidePage {
|
||||
}
|
||||
|
||||
class _IntroDetailState extends CommonCollapseSlidePageState<IntroDetail> {
|
||||
late final _tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
return Material(
|
||||
color: theme.colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: Get.back,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 35,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onSecondaryContainer
|
||||
.withValues(alpha: 0.5),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3))),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
dividerHeight: 0,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: const [Tab(text: '详情'), Tab(text: '点评')],
|
||||
),
|
||||
),
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: Get.back,
|
||||
iconSize: 22,
|
||||
bgColor: Colors.transparent,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: enableSlide ? slideList(theme) : buildList(theme),
|
||||
)
|
||||
child: tabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
buildList(theme),
|
||||
PgcReviewPage(
|
||||
name: widget.bangumiDetail.title!,
|
||||
mediaId: widget.bangumiDetail.mediaId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -64,6 +90,7 @@ class _IntroDetailState extends CommonCollapseSlidePageState<IntroDetail> {
|
||||
padding: EdgeInsets.only(
|
||||
left: 14,
|
||||
right: 14,
|
||||
top: 14,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
children: [
|
||||
|
||||
@@ -174,7 +174,7 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
if (index == response.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return _itemWidget(context, theme, response[index]);
|
||||
return _itemWidget(theme, response[index]);
|
||||
},
|
||||
itemCount: response!.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
@@ -189,91 +189,92 @@ class _NoteListPageState extends CommonSlidePageState<NoteListPage> {
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Widget _itemWidget(BuildContext context, ThemeData theme, dynamic item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (item['web_url'] != null && item['web_url'] != '') {
|
||||
PiliScheme.routePushFromUrl(item['web_url']);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.toNamed('/member?mid=${item['author']['mid']}'),
|
||||
child: NetworkImgLayer(
|
||||
height: 34,
|
||||
width: 34,
|
||||
src: item['author']['face'],
|
||||
type: ImageType.avatar,
|
||||
Widget _itemWidget(ThemeData theme, dynamic item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (item['web_url'] != null && item['web_url'] != '') {
|
||||
PiliScheme.routePushFromUrl(item['web_url']);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.toNamed('/member?mid=${item['author']['mid']}'),
|
||||
child: NetworkImgLayer(
|
||||
height: 34,
|
||||
width: 34,
|
||||
src: item['author']['face'],
|
||||
type: ImageType.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
Get.toNamed('/member?mid=${item['author']['mid']}'),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
item['author']['name'],
|
||||
style: TextStyle(
|
||||
color: (item['author']?['vip_info']?['status'] ?? 0) >
|
||||
0 &&
|
||||
item['author']?['vip_info']?['type'] == 2
|
||||
? context.vipColor
|
||||
: theme.colorScheme.outline,
|
||||
fontSize: 13,
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
Get.toNamed('/member?mid=${item['author']['mid']}'),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
item['author']['name'],
|
||||
style: TextStyle(
|
||||
color: (item['author']?['vip_info']?['status'] ??
|
||||
0) >
|
||||
0 &&
|
||||
item['author']?['vip_info']?['type'] == 2
|
||||
? context.vipColor
|
||||
: theme.colorScheme.outline,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${item['author']['level']}.png',
|
||||
height: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item['pubtime'],
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (item['summary'] != null) ...[
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
item['summary'],
|
||||
style: TextStyle(
|
||||
height: 1.75,
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
const SizedBox(width: 6),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${item['author']['level']}.png',
|
||||
height: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (item['web_url'] != null && item['web_url'] != '')
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item['pubtime'],
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (item['summary'] != null) ...[
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'查看全部',
|
||||
item['summary'],
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
height: 1.75,
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
),
|
||||
),
|
||||
if (item['web_url'] != null && item['web_url'] != '')
|
||||
Text(
|
||||
'查看全部',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
height: 1.75,
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,24 +94,23 @@ class _ZanButtonGrpcState extends State<ZanButtonGrpc> {
|
||||
}
|
||||
}
|
||||
|
||||
ButtonStyle get _style => TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final Color color = theme.colorScheme.outline;
|
||||
final Color primary = theme.colorScheme.primary;
|
||||
final ButtonStyle style = TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
style: _style,
|
||||
style: style,
|
||||
onPressed: () => handleState(onHateReply),
|
||||
child: Icon(
|
||||
isDislike
|
||||
@@ -126,7 +125,7 @@ class _ZanButtonGrpcState extends State<ZanButtonGrpc> {
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
style: _style,
|
||||
style: style,
|
||||
onPressed: () => handleState(onLikeReply),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -2071,7 +2071,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
);
|
||||
}
|
||||
|
||||
void showIntroDetail(videoDetail, videoTags) {
|
||||
void showIntroDetail(bangumi.BangumiInfoModel videoDetail, videoTags) {
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => bangumi.IntroDetail(
|
||||
|
||||
@@ -157,7 +157,8 @@ class RequestUtils {
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
sheetAnimationStyle: const AnimationStyle(curve: Curves.ease),
|
||||
sheetAnimationStyle:
|
||||
const AnimationStyle(curve: Curves.ease),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(640, min(Get.width, Get.height)),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user