opt req

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-08-15 09:52:25 +08:00
parent 1943b65788
commit 422b413778
51 changed files with 923 additions and 1143 deletions

View File

@@ -33,7 +33,7 @@ class MultiSelectAppBarWidget extends StatelessWidget
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => ctr.handleSelect(true),
onPressed: () => ctr.handleSelect(checked: true),
child: const Text('全选'),
),
...?children,

View File

@@ -1,14 +1,18 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:flutter/material.dart';
Widget selectMask(ThemeData theme, bool checked) {
Widget selectMask(
ThemeData theme,
bool checked, {
BorderRadiusGeometry borderRadius = StyleString.mdRadius,
}) {
return AnimatedOpacity(
opacity: checked ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: StyleString.mdRadius,
borderRadius: borderRadius,
color: Colors.black.withValues(alpha: 0.6),
),
child: AnimatedScale(

View File

@@ -134,9 +134,9 @@ class Api {
// aid num 稿件avid 必要(可选) avid与bvid任选一个
// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
// csrf str CSRF Token位于cookie 必要
static const String oneThree = '/x/web-interface/archive/like/triple';
static const String ugcTriple = '/x/web-interface/archive/like/triple';
static const String triple = '/pgc/season/episode/like/triple';
static const String pgcTriple = '/pgc/season/episode/like/triple';
// 获取指定用户创建的所有收藏夹信息
// 该接口也能查询目标内容id存在于那些收藏夹中

View File

@@ -27,7 +27,7 @@ class DanmakuHttp {
// assert(aid != null || bvid != null);
// assert(csrf != null || access_key != null);
// 构建参数对象
var params = <String, dynamic>{
var data = <String, dynamic>{
'type': type,
'oid': oid,
'msg': msg,
@@ -47,10 +47,8 @@ class DanmakuHttp {
var response = await Request().post(
Api.shootDanmaku,
data: params,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
data: data,
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (response.statusCode != 200) {
return {

View File

@@ -2,6 +2,7 @@ import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/models/user/danmaku_block.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:dio/dio.dart';
class DanmakuFilterHttp {
static Future danmakuFilter() async {
@@ -22,10 +23,11 @@ class DanmakuFilterHttp {
static Future danmakuFilterDel({required int ids}) async {
var res = await Request().post(
Api.danmakuFilterDel,
queryParameters: {
data: {
'ids': ids,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -43,11 +45,12 @@ class DanmakuFilterHttp {
}) async {
var res = await Request().post(
Api.danmakuFilterAdd,
queryParameters: {
data: {
'type': type,
'filter': filter,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {

View File

@@ -103,18 +103,20 @@ class FavHttp {
var res = type == 11
? await Request().post(
Api.unfavFolder,
queryParameters: {
data: {
'media_id': id,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
)
: await Request().post(
Api.unfavSeason,
queryParameters: {
data: {
'platform': 'web',
'season_id': id,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};

View File

@@ -420,9 +420,7 @@ class LiveHttp {
var res = await Request().post(
Api.setLiveFavTag,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {

View File

@@ -28,7 +28,6 @@ import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/wbi_sign.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
class MemberHttp {
static Future reportMember(
@@ -38,16 +37,14 @@ class MemberHttp {
}) async {
var res = await Request().post(
HttpString.spaceBaseUrl + Api.reportMember,
data: FormData.fromMap(
{
'mid': mid,
'reason': reason,
'reason_v2': ?reasonV2,
'csrf': Accounts.main.csrf,
},
),
data: {
'mid': mid,
'reason': reason,
'reason_v2': ?reasonV2,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
debugPrint('report: ${res.data}');
return {
'status': res.data['status'],
'msg': res.data['message'] ?? res.data['data'],
@@ -510,9 +507,7 @@ class MemberHttp {
'fid': fid,
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -538,9 +533,7 @@ class MemberHttp {
'csrf': Accounts.main.csrf,
// 'cross_domain': true
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '操作成功'};
@@ -591,9 +584,7 @@ class MemberHttp {
'tag': tagName,
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -614,9 +605,7 @@ class MemberHttp {
'name': name,
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -636,9 +625,7 @@ class MemberHttp {
'tagid': tagid,
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};

View File

@@ -187,16 +187,14 @@ class MsgHttp {
String? biz,
CancelToken? cancelToken,
}) async {
final file = await MultipartFile.fromFile(path);
Map<String, dynamic> data = {
'file_up': file,
'category': ?category,
'biz': ?biz,
'csrf': Accounts.main.csrf,
};
var res = await Request().post(
Api.uploadBfs,
data: FormData.fromMap(data),
data: FormData.fromMap({
'file_up': await MultipartFile.fromFile(path),
'category': ?category,
'biz': ?biz,
'csrf': Accounts.main.csrf,
}),
cancelToken: cancelToken,
);
if (res.data['code'] == 0) {
@@ -226,7 +224,8 @@ class MsgHttp {
});
var res = await Request().post(
HttpString.tUrl + Api.createTextDynamic,
data: FormData.fromMap(data),
data: data,
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -272,7 +271,8 @@ class MsgHttp {
});
var res = await Request().post(
HttpString.tUrl + Api.removeMsg,
data: FormData.fromMap(data),
data: data,
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -296,9 +296,7 @@ class MsgHttp {
'csrf_token': csrf,
'csrf': csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -354,7 +352,8 @@ class MsgHttp {
});
var res = await Request().post(
HttpString.tUrl + Api.setTop,
data: FormData.fromMap(data),
data: data,
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -407,7 +406,7 @@ class MsgHttp {
}) async {
String csrf = Accounts.main.csrf;
final devId = getDevId();
Map<String, dynamic> base = {
Map<String, dynamic> data = {
'msg': {
'sender_uid': senderUid,
'receiver_id': receiverId,
@@ -425,7 +424,7 @@ class MsgHttp {
'csrf_token': csrf,
'csrf': csrf,
};
Map<String, dynamic> params = await WbiSign.makSign(base);
Map<String, dynamic> params = await WbiSign.makSign(data);
var res = await Request().post(
Api.sendMsg,
queryParameters: <String, dynamic>{
@@ -435,7 +434,7 @@ class MsgHttp {
'w_rid': params['w_rid'],
'wts': params['wts'],
},
data: base,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),

View File

@@ -196,9 +196,7 @@ class ReplyHttp {
'action': action,
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
@@ -216,13 +214,14 @@ class ReplyHttp {
}) async {
var res = await Request().post(
Api.likeReply,
queryParameters: {
data: {
'type': type,
'oid': oid,
'rpid': rpid,
'action': action,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
@@ -263,9 +262,7 @@ class ReplyHttp {
'action': isUpTop ? 0 : 1,
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};

View File

@@ -106,12 +106,15 @@ class UserHttp {
account ??= Accounts.history;
var res = await Request().post(
Api.pauseHistory,
queryParameters: {
data: {
'switch': switchStatus,
'jsonp': 'jsonp',
'csrf': account.csrf,
},
options: Options(extra: {'account': account}),
options: Options(
extra: {'account': account},
contentType: Headers.formUrlEncodedContentType,
),
);
return res;
}
@@ -134,11 +137,14 @@ class UserHttp {
account ??= Accounts.history;
var res = await Request().post(
Api.clearHistory,
queryParameters: {
data: {
'jsonp': 'jsonp',
'csrf': account.csrf,
},
options: Options(extra: {'account': account}),
options: Options(
extra: {'account': account},
contentType: Headers.formUrlEncodedContentType,
),
);
return res;
}
@@ -147,11 +153,12 @@ class UserHttp {
static Future toViewLater({String? bvid, dynamic aid}) async {
var res = await Request().post(
Api.toViewLater,
queryParameters: {
data: {
'aid': ?aid,
'bvid': ?bvid,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': 'yeah稍后再看'};
@@ -201,10 +208,11 @@ class UserHttp {
static Future toViewClear([int? cleanType]) async {
var res = await Request().post(
Api.toViewClear,
queryParameters: {
data: {
'clean_type': ?cleanType,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '操作完成'};
@@ -369,9 +377,7 @@ class UserHttp {
"reason_type": reasonType,
"reason_desc": reasonType == 0 ? reasonDesc : null,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
options: Options(contentType: Headers.formUrlEncodedContentType),
);
return res.data as Map;
}

View File

@@ -360,9 +360,9 @@ class VideoHttp {
}
// 一键三连 pgc
static Future triple({dynamic epId, required dynamic seasonId}) async {
static Future pgcTriple({dynamic epId, required dynamic seasonId}) async {
var res = await Request().post(
Api.triple,
Api.pgcTriple,
data: {
'ep_id': epId,
'csrf': Accounts.main.csrf,
@@ -384,9 +384,9 @@ class VideoHttp {
}
// 一键三连
static Future oneThree({required String bvid}) async {
static Future ugcTriple({required String bvid}) async {
var res = await Request().post(
Api.oneThree,
Api.ugcTriple,
data: {
'aid': IdUtils.bv2av(bvid),
'eab_x': 2,
@@ -394,6 +394,8 @@ class VideoHttp {
'source': 'web_normal',
'ga': 1,
'csrf': Accounts.main.csrf,
'spmid': '333.788.0.0',
'statistics': '{"appId":100,"platform":5}',
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
@@ -529,7 +531,7 @@ class VideoHttp {
if (message == '') {
return {'status': false, 'msg': '请输入评论内容'};
}
Map<String, dynamic> data = {
final data = {
'type': type,
'oid': oid,
if (root != null && root != 0) 'root': root,
@@ -560,12 +562,13 @@ class VideoHttp {
}) async {
var res = await Request().post(
Api.replyDel,
queryParameters: {
data: {
'type': type, //type.index
'oid': oid,
'rpid': rpid,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
@@ -637,11 +640,12 @@ class VideoHttp {
}) async {
await Request().post(
Api.historyReport,
queryParameters: {
data: {
'aid': ?aid,
'type': ?type,
'csrf': Accounts.heartbeat.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
}
@@ -659,7 +663,7 @@ class VideoHttp {
final isPugv = videoType == VideoType.pugv;
await Request().post(
Api.heartBeat,
queryParameters: {
data: {
if (isPugv) 'aid': ?aid else 'bvid': ?bvid,
'cid': cid,
'epid': ?epid,
@@ -669,6 +673,7 @@ class VideoHttp {
'played_time': progress,
'csrf': Accounts.heartbeat.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
}
@@ -679,12 +684,13 @@ class VideoHttp {
}) async {
await Request().post(
Api.mediaListHistory,
queryParameters: {
data: {
'desc': desc,
'oid': oid,
'upper_mid': upperMid,
'csrf': Accounts.heartbeat.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
}
@@ -692,10 +698,11 @@ class VideoHttp {
static Future pgcAdd({int? seasonId}) async {
var res = await Request().post(
Api.pgcAdd,
queryParameters: {
data: {
'season_id': seasonId,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {
@@ -718,10 +725,11 @@ class VideoHttp {
static Future pgcDel({int? seasonId}) async {
var res = await Request().post(
Api.pgcDel,
queryParameters: {
data: {
'season_id': seasonId,
'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {

View File

@@ -16,7 +16,6 @@ import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/url_utils.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -227,22 +226,6 @@ class ArticleController extends CommonDynController<MainListReply> {
SmartDialog.showToast(res['msg']);
}
}
@override
void listener() {
showTitle.value = scrollController.positions.last.pixels >= 45;
final ScrollDirection direction1 =
scrollController.positions.first.userScrollDirection;
late final ScrollDirection direction2 =
scrollController.positions.last.userScrollDirection;
if (direction1 == ScrollDirection.forward ||
direction2 == ScrollDirection.forward) {
showFab();
} else if (direction1 == ScrollDirection.reverse ||
direction2 == ScrollDirection.reverse) {
hideFab();
}
}
}
class Summary {

View File

@@ -81,112 +81,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
child: Stack(
clipBehavior: Clip.none,
children: [
SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) {
double padding = max(
context.width / 2 - Grid.smallCardWidth,
0,
);
if (isPortrait) {
return LayoutBuilder(
builder: (context, constraints) {
final maxWidth =
constraints.maxWidth - 2 * padding - 24;
return Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: CustomScrollView(
controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
_buildContent(theme, maxWidth),
SliverToBoxAdapter(
child: Divider(
thickness: 8,
color: theme.dividerColor.withValues(
alpha: 0.05,
),
),
),
buildReplyHeader(theme),
Obx(
() => _buildReplyList(
theme,
controller.loadingState.value,
),
),
],
),
);
},
);
} else {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: controller.ratio[0].toInt(),
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth =
constraints.maxWidth - padding / 4 - 24;
return CustomScrollView(
controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
left: padding / 4,
bottom:
MediaQuery.paddingOf(context).bottom +
80,
),
sliver: _buildContent(theme, maxWidth),
),
],
);
},
),
),
VerticalDivider(
thickness: 8,
color: theme.dividerColor.withValues(alpha: 0.05),
),
Expanded(
flex: controller.ratio[1].toInt(),
child: Scaffold(
key: scaffoldKey,
backgroundColor: Colors.transparent,
body: refreshIndicator(
onRefresh: controller.onRefresh,
child: Padding(
padding: EdgeInsets.only(right: padding / 4),
child: CustomScrollView(
controller: controller.scrollController,
physics:
const AlwaysScrollableScrollPhysics(),
slivers: [
buildReplyHeader(theme),
Obx(
() => _buildReplyList(
theme,
controller.loadingState.value,
),
),
],
),
),
),
),
),
],
);
}
},
),
),
_buildPage(theme, isPortrait),
_buildBottom(theme),
],
),
@@ -194,6 +89,103 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
);
}
Widget _buildPage(ThemeData theme, bool isPortrait) {
return LayoutBuilder(
builder: (context, constraints) {
double padding = max(
context.width / 2 - Grid.smallCardWidth,
0,
);
if (isPortrait) {
final maxWidth = constraints.maxWidth - 2 * padding - 24;
return Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: CustomScrollView(
controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
_buildContent(theme, maxWidth),
SliverToBoxAdapter(
child: Divider(
thickness: 8,
color: theme.dividerColor.withValues(
alpha: 0.05,
),
),
),
buildReplyHeader(theme),
Obx(
() => _buildReplyList(
theme,
controller.loadingState.value,
),
),
],
),
);
}
padding = padding / 4;
final flex = controller.ratio[0].toInt();
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: flex,
child: CustomScrollView(
controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
left: padding,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: _buildContent(
theme,
constraints.maxWidth * flex - padding - 24,
),
),
],
),
),
VerticalDivider(
thickness: 8,
color: theme.dividerColor.withValues(alpha: 0.05),
),
Expanded(
flex: controller.ratio[1].toInt(),
child: Scaffold(
key: scaffoldKey,
backgroundColor: Colors.transparent,
body: refreshIndicator(
onRefresh: controller.onRefresh,
child: Padding(
padding: EdgeInsets.only(right: padding),
child: CustomScrollView(
controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
buildReplyHeader(theme),
Obx(
() => _buildReplyList(
theme,
controller.loadingState.value,
),
),
],
),
),
),
),
),
],
);
},
);
}
Widget _buildContent(ThemeData theme, double maxWidth) => SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
sliver: Obx(

View File

@@ -1,6 +1,5 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/image/image_view.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
@@ -73,75 +72,71 @@ class OpusContent extends StatelessWidget {
switch (element.paraType) {
case 1 || 4:
final isQuote = element.paraType == 4;
Widget widget = SelectionArea(
child: Text.rich(
textAlign: element.align == 1 ? TextAlign.center : null,
TextSpan(
children: element.text?.nodes?.map((item) {
switch (item.type) {
case 'TEXT_NODE_TYPE_RICH' when (item.rich != null):
Rich rich = item.rich!;
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_EMOJI':
Emoji emoji = rich.emoji!;
final size = 20.0 * emoji.size;
return WidgetSpan(
child: NetworkImgLayer(
width: size,
height: size,
src: emoji.url,
type: ImageType.emote,
),
);
default:
return TextSpan(
text:
'${rich.type == 'RICH_TEXT_NODE_TYPE_WEB' ? '\u{1F517}' : ''}${item.rich!.text}',
style: _getStyle(
rich.style,
rich.type == 'RICH_TEXT_NODE_TYPE_TEXT'
? null
: colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_AT':
Get.toNamed('/member?mid=${rich.rid}');
// case 'RICH_TEXT_NODE_TYPE_TOPIC':
default:
if (rich.jumpUrl != null) {
PiliScheme.routePushFromUrl(
rich.jumpUrl!,
);
}
}
},
);
}
case 'TEXT_NODE_TYPE_FORMULA'
when (item.formula != null):
return WidgetSpan(
child: CachedNetworkSVGImage(
height: 65,
'https://api.bilibili.com/x/web-frontend/mathjax/tex?formula=${Uri.encodeComponent(item.formula!.latexContent!)}',
colorFilter: ColorFilter.mode(
colorScheme.onSurfaceVariant,
BlendMode.srcIn,
Widget widget = SelectableText.rich(
textAlign: element.align == 1 ? TextAlign.center : null,
TextSpan(
children: element.text?.nodes?.map((item) {
switch (item.type) {
case 'TEXT_NODE_TYPE_RICH' when (item.rich != null):
Rich rich = item.rich!;
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_EMOJI':
Emoji emoji = rich.emoji!;
final size = 20.0 * emoji.size;
return WidgetSpan(
child: NetworkImgLayer(
width: size,
height: size,
src: emoji.url,
type: ImageType.emote,
),
alignment: Alignment.centerLeft,
placeholderBuilder: (_) =>
const SizedBox.shrink(),
);
default:
return TextSpan(
text:
'${rich.type == 'RICH_TEXT_NODE_TYPE_WEB' ? '\u{1F517}' : ''}${item.rich!.text}',
style: _getStyle(
rich.style,
rich.type == 'RICH_TEXT_NODE_TYPE_TEXT'
? null
: colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_AT':
Get.toNamed('/member?mid=${rich.rid}');
// case 'RICH_TEXT_NODE_TYPE_TOPIC':
default:
if (rich.jumpUrl != null) {
PiliScheme.routePushFromUrl(
rich.jumpUrl!,
);
}
}
},
);
}
case 'TEXT_NODE_TYPE_FORMULA' when (item.formula != null):
return WidgetSpan(
child: CachedNetworkSVGImage(
height: 65,
'https://api.bilibili.com/x/web-frontend/mathjax/tex?formula=${Uri.encodeComponent(item.formula!.latexContent!)}',
colorFilter: ColorFilter.mode(
colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
);
default:
return _getSpan(
item.word,
isQuote ? colorScheme.onSurfaceVariant : null,
);
}
}).toList(),
),
alignment: Alignment.centerLeft,
placeholderBuilder: (_) => const SizedBox.shrink(),
),
);
default:
return _getSpan(
item.word,
isQuote ? colorScheme.onSurfaceVariant : null,
);
}
}).toList(),
),
);
if (isQuote) {
@@ -223,25 +218,23 @@ class OpusContent extends StatelessWidget {
imageUrl: ImageUtil.thumbnailUrl(element.line!.pic!.url!),
);
case 5 when (element.list != null):
return SelectionArea(
child: Text.rich(
TextSpan(
children: element.list!.items?.indexed.map((entry) {
return TextSpan(
children: [
const WidgetSpan(
child: Icon(MdiIcons.circleMedium),
alignment: PlaceholderAlignment.middle,
),
...entry.$2.nodes!.map((item) {
return _getSpan(item.word);
}),
if (entry.$1 < element.list!.items!.length - 1)
const TextSpan(text: '\n'),
],
);
}).toList(),
),
return SelectableText.rich(
TextSpan(
children: element.list!.items?.indexed.map((entry) {
return TextSpan(
children: [
const WidgetSpan(
child: Icon(MdiIcons.circleMedium),
alignment: PlaceholderAlignment.middle,
),
...entry.$2.nodes!.map((item) {
return _getSpan(item.word);
}),
if (entry.$1 < element.list!.items!.length - 1)
const TextSpan(text: '\n'),
],
);
}).toList(),
),
);
case 6:
@@ -292,7 +285,7 @@ class OpusContent extends StatelessWidget {
children: [
NetworkImgLayer(
radius: 6,
width: 65 * StyleString.aspectRatio,
width: 104,
height: 65,
src: element.linkCard!.card!.ugc!.cover,
),
@@ -331,7 +324,7 @@ class OpusContent extends StatelessWidget {
children: [
NetworkImgLayer(
radius: 6,
width: 65 * StyleString.aspectRatio,
width: 104,
height: 65,
src: element.linkCard!.card!.common!.cover,
),
@@ -368,7 +361,7 @@ class OpusContent extends StatelessWidget {
children: [
NetworkImgLayer(
radius: 6,
width: 65 * StyleString.aspectRatio,
width: 104,
height: 65,
src: element.linkCard!.card!.live!.cover,
),
@@ -405,7 +398,7 @@ class OpusContent extends StatelessWidget {
children: [
NetworkImgLayer(
radius: 6,
width: 65 * StyleString.aspectRatio,
width: 104,
height: 65,
src: element.linkCard!.card!.opus!.cover,
),
@@ -466,7 +459,7 @@ class OpusContent extends StatelessWidget {
children: [
NetworkImgLayer(
radius: 6,
width: 65 * StyleString.aspectRatio,
width: 104,
height: 65,
src: element.linkCard!.card!.music!.cover,
),
@@ -504,7 +497,7 @@ class OpusContent extends StatelessWidget {
children: [
NetworkImgLayer(
radius: 6,
width: 65 * StyleString.aspectRatio,
width: 104,
height: 65,
src: e.cover,
),
@@ -570,41 +563,35 @@ class OpusContent extends StatelessWidget {
color: colorScheme.onInverseSurface,
),
width: double.infinity,
child: SelectionArea(child: Text.rich(renderer.span!)),
child: SelectableText.rich(renderer.span!),
);
default:
if (kDebugMode) debugPrint('unknown type ${element.paraType}');
if (element.text?.nodes?.isNotEmpty == true) {
return SelectionArea(
child: Text.rich(
textAlign: element.align == 1 ? TextAlign.center : null,
TextSpan(
children: element.text!.nodes!
.map<TextSpan>((item) => _getSpan(item.word))
.toList(),
),
return SelectableText.rich(
textAlign: element.align == 1 ? TextAlign.center : null,
TextSpan(
children: element.text!.nodes!
.map<TextSpan>((item) => _getSpan(item.word))
.toList(),
),
);
}
return SelectionArea(
child: Text(
'不支持的类型 (${element.paraType})',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
return SelectableText(
'不支持的类型 (${element.paraType})',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
);
}
} catch (e) {
return SelectionArea(
child: Text(
'错误的类型 $e',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
return SelectableText(
'错误的类型 $e',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
);
}
@@ -676,7 +663,7 @@ Widget moduleBlockedItem(
CachedNetworkImage(
height: 16,
color: Colors.white,
imageUrl: moduleBlocked.button!.icon!,
imageUrl: moduleBlocked.button!.icon!.http2https,
),
Text(moduleBlocked.button!.text ?? ''),
],

View File

@@ -12,8 +12,8 @@ abstract class MultiSelectBase<T> {
int get checkedCount;
void onSelect(T item, [bool disableSelect = true]);
void handleSelect([bool checked = false, bool disableSelect = true]);
void onSelect(T item);
void handleSelect({bool checked = false, bool disableSelect = true});
void onRemove();
}
@@ -34,7 +34,7 @@ mixin CommonMultiSelectMixin<T extends MultiSelectData>
loadingState.value.data!.where((v) => v.checked == true);
@override
void onSelect(T item, [bool disableSelect = true]) {
void onSelect(T item) {
List<T> list = loadingState.value.data!;
item.checked = !(item.checked ?? false);
if (item.checked!) {
@@ -43,17 +43,15 @@ mixin CommonMultiSelectMixin<T extends MultiSelectData>
rxCount.value--;
}
loadingState.refresh();
if (disableSelect) {
if (checkedCount == 0) {
enableMultiSelect.value = false;
}
if (checkedCount == 0) {
enableMultiSelect.value = false;
} else {
allSelected.value = checkedCount == list.length;
}
}
@override
void handleSelect([bool checked = false, bool disableSelect = true]) {
void handleSelect({bool checked = false, bool disableSelect = true}) {
if (loadingState.value.isSuccess) {
final list = loadingState.value.data;
if (list?.isNotEmpty == true) {

View File

@@ -14,7 +14,7 @@ Widget livePanelSub(
int floor = 1,
required double maxWidth,
}) {
maxWidth -= StyleString.safeSpace * 2;
maxWidth -= 24;
SubscriptionNew? subItem = item.modules.moduleDynamic!.major?.subscriptionNew;
LivePlayInfo? content = subItem?.liveRcmd?.content?.livePlayInfo;
if (subItem == null || content == null) {

View File

@@ -14,7 +14,7 @@ Widget liveRcmdPanel(
int floor = 1,
required double maxWidth,
}) {
maxWidth -= StyleString.safeSpace * 2;
maxWidth -= 24;
DynamicLiveModel? liveRcmd = item.modules.moduleDynamic?.major?.liveRcmd;
if (liveRcmd == null) {
return const SizedBox.shrink();

View File

@@ -1,7 +1,5 @@
import 'dart:async';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/dynamic_card.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';
@@ -28,46 +26,11 @@ class DynamicsTabPage extends CommonPage {
@override
State<DynamicsTabPage> createState() => _DynamicsTabPageState();
static Widget dynSkeleton(bool dynamicsWaterfallFlow) {
if (!dynamicsWaterfallFlow) {
return SliverCrossAxisGroup(
slivers: [
const SliverFillRemaining(),
SliverConstrainedCrossAxis(
maxExtent: Grid.smallCardWidth * 2,
sliver: SliverList.builder(
itemBuilder: (context, index) {
return const DynamicCardSkeleton();
},
itemCount: 10,
),
),
const SliverFillRemaining(),
],
);
}
return SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
crossAxisSpacing: StyleString.cardSpace / 2,
mainAxisSpacing: StyleString.cardSpace / 2,
maxCrossAxisExtent: Grid.smallCardWidth * 2,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent: 50,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return const DynamicCardSkeleton();
},
childCount: 10,
),
);
}
}
class _DynamicsTabPageState
extends CommonPageState<DynamicsTabPage, DynamicsTabController>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, DynMixin {
StreamSubscription? _listener;
late final MainController _mainController = Get.find<MainController>();
@@ -135,23 +98,14 @@ class _DynamicsTabPageState
);
}
late double _maxWidth;
Widget _buildBody(LoadingState<List<DynamicItemModel>?> loadingState) {
return switch (loadingState) {
Loading() => DynamicsTabPage.dynSkeleton(
GlobalData().dynamicsWaterfallFlow,
),
Loading() => dynSkeleton,
Success(:var response) =>
response?.isNotEmpty == true
? GlobalData().dynamicsWaterfallFlow
? SliverWaterfallFlow(
gridDelegate:
SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
crossAxisSpacing: StyleString.cardSpace / 2,
callback: (value) => _maxWidth = value,
),
gridDelegate: gridDelegate,
delegate: SliverChildBuilderDelegate(
(_, index) {
if (index == response.length - 1) {
@@ -162,7 +116,7 @@ class _DynamicsTabPageState
onRemove: (idStr) =>
controller.onRemove(index, idStr),
onBlock: () => controller.onBlock(index),
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
},
childCount: response!.length,
@@ -184,7 +138,7 @@ class _DynamicsTabPageState
onRemove: (idStr) =>
controller.onRemove(index, idStr),
onBlock: () => controller.onBlock(index),
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
},
itemCount: response!.length,

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart';
@@ -12,7 +11,6 @@ import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/dynamics_create/view.dart';
import 'package:PiliPlus/pages/dynamics_tab/view.dart';
import 'package:PiliPlus/pages/dynamics_topic/controller.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/grid.dart';
@@ -35,7 +33,7 @@ class DynTopicPage extends StatefulWidget {
State<DynTopicPage> createState() => _DynTopicPageState();
}
class _DynTopicPageState extends State<DynTopicPage> {
class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
final DynTopicController _controller = Get.put(
DynTopicController(),
tag: Utils.generateRandomString(8),
@@ -346,23 +344,14 @@ class _DynTopicPageState extends State<DynTopicPage> {
};
}
late double _maxWidth;
Widget _buildBody(LoadingState<List<TopicCardItem>?> loadingState) {
return switch (loadingState) {
Loading() => DynamicsTabPage.dynSkeleton(
GlobalData().dynamicsWaterfallFlow,
),
Loading() => dynSkeleton,
Success(:var response) =>
response?.isNotEmpty == true
? GlobalData().dynamicsWaterfallFlow
? SliverWaterfallFlow(
gridDelegate:
SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
crossAxisSpacing: StyleString.cardSpace / 2,
callback: (value) => _maxWidth = value,
),
gridDelegate: gridDelegate,
delegate: SliverChildBuilderDelegate(
(_, index) {
if (index == response.length - 1) {
@@ -373,7 +362,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
if (item.dynamicCardItem != null) {
return DynamicPanel(
item: item.dynamicCardItem!,
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
}
@@ -396,7 +385,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
if (item.dynamicCardItem != null) {
return DynamicPanel(
item: item.dynamicCardItem!,
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
} else {
return Text(item.topicType ?? 'err');

View File

@@ -246,6 +246,7 @@ class _EpisodePanelState extends CommonCollapseSlidePageState<EpisodePanel> {
}
Widget _buildBody(ThemeData theme, int tabIndex, episodes) {
final isCurrTab = tabIndex == widget.initialTabIndex;
return KeepAliveWrapper(
builder: (context) => ScrollablePositionedList.separated(
padding: EdgeInsets.only(
@@ -254,7 +255,7 @@ class _EpisodePanelState extends CommonCollapseSlidePageState<EpisodePanel> {
),
reverse: _isReversed[tabIndex],
itemCount: episodes.length,
initialScrollIndex: _currentItemIndex,
initialScrollIndex: isCurrTab ? _currentItemIndex : 0,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int itemIndex) {
final episode = episodes[itemIndex];
@@ -263,9 +264,7 @@ class _EpisodePanelState extends CommonCollapseSlidePageState<EpisodePanel> {
episode: episode,
index: itemIndex,
length: episodes.length,
isCurrentIndex: tabIndex == widget.initialTabIndex
? itemIndex == _currentItemIndex
: false,
isCurrentIndex: isCurrTab ? itemIndex == _currentItemIndex : false,
);
return widget.type == EpisodeType.season &&
widget.showTitle &&
@@ -394,47 +393,40 @@ class _EpisodePanelState extends CommonCollapseSlidePageState<EpisodePanel> {
spacing: 10,
children: [
if (cover?.isNotEmpty == true)
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
),
if (duration != null && duration > 0)
PBadge(
text: DurationUtil.formatDuration(duration),
right: 6.0,
bottom: 6.0,
type: PBadgeType.gray,
),
if (isCharging == true)
const PBadge(
text: '充电专属',
top: 6,
right: 6,
type: PBadgeType.error,
)
else if (episode.badge != null)
PBadge(
text: episode.badge,
top: 6,
right: 6,
type: switch (episode.badge) {
'会员' => PBadgeType.primary,
'限免' => PBadgeType.free,
_ => PBadgeType.gray,
},
),
],
);
},
),
Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: cover,
width: 140.8,
height: 88,
),
if (duration != null && duration > 0)
PBadge(
text: DurationUtil.formatDuration(duration),
right: 6.0,
bottom: 6.0,
type: PBadgeType.gray,
),
if (isCharging == true)
const PBadge(
text: '充电专属',
top: 6,
right: 6,
type: PBadgeType.error,
)
else if (episode.badge != null)
PBadge(
text: episode.badge,
top: 6,
right: 6,
type: switch (episode.badge) {
'会员' => PBadgeType.primary,
'限免' => PBadgeType.free,
_ => PBadgeType.gray,
},
),
],
)
else if (isCurrentIndex)
Image.asset(

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
@@ -38,7 +37,7 @@ class _FavArticlePageState extends State<FavArticlePage>
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(

View File

@@ -32,108 +32,109 @@ class _FavNoteChildPageState extends State<FavNoteChildPage>
super.build(context);
final theme = Theme.of(context);
final padding = MediaQuery.paddingOf(context);
return LayoutBuilder(
builder: (context, constraints) => Stack(
clipBehavior: Clip.none,
children: [
refreshIndicator(
onRefresh: _favNoteController.onRefresh,
child: CustomScrollView(
controller: _favNoteController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(bottom: padding.bottom + 80),
sliver: Obx(
() => _buildBody(_favNoteController.loadingState.value),
),
final bottomH = 50 + padding.bottom;
return Stack(
clipBehavior: Clip.none,
children: [
refreshIndicator(
onRefresh: _favNoteController.onRefresh,
child: CustomScrollView(
controller: _favNoteController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(bottom: padding.bottom + 80),
sliver: Obx(
() => _buildBody(_favNoteController.loadingState.value),
),
],
),
),
],
),
Positioned(
top: constraints.maxHeight,
left: 0,
right: 0,
child: Obx(
() => AnimatedSlide(
offset: _favNoteController.enableMultiSelect.value
? const Offset(0, -1)
: Offset.zero,
duration: const Duration(milliseconds: 150),
child: Container(
padding: padding,
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
border: Border(
top: BorderSide(
width: 0.5,
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
Positioned(
left: 0,
right: 0,
bottom: -bottomH,
child: Obx(
() => AnimatedSlide(
offset: _favNoteController.enableMultiSelect.value
? const Offset(0, -1)
: Offset.zero,
duration: const Duration(milliseconds: 150),
child: Container(
height: bottomH,
padding: padding,
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
border: Border(
top: BorderSide(
width: 0.5,
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 16),
iconButton(
size: 32,
tooltip: '取消',
context: context,
icon: Icons.clear,
onPressed: _favNoteController.onDisable,
),
const SizedBox(width: 12),
Obx(
() => Checkbox(
value: _favNoteController.allSelected.value,
onChanged: (value) {
_favNoteController.handleSelect(
!_favNoteController.allSelected.value,
);
},
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _favNoteController.handleSelect(
!_favNoteController.allSelected.value,
),
child: const Padding(
padding: EdgeInsets.only(
top: 14,
bottom: 14,
right: 12,
),
child: Text('全选'),
),
),
const Spacer(),
FilledButton.tonal(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
if (_favNoteController.checkedCount != 0) {
showConfirmDialog(
context: context,
title: '确定删除已选中的笔记吗?',
onConfirm: _favNoteController.onRemove,
);
}
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 16),
iconButton(
size: 32,
tooltip: '取消',
context: context,
icon: Icons.clear,
onPressed: _favNoteController.onDisable,
),
const SizedBox(width: 12),
Obx(
() => Checkbox(
value: _favNoteController.allSelected.value,
onChanged: (value) {
_favNoteController.handleSelect(
checked: !_favNoteController.allSelected.value,
);
},
child: const Text('删除'),
),
const SizedBox(width: 16),
],
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _favNoteController.handleSelect(
checked: !_favNoteController.allSelected.value,
disableSelect: false,
),
child: const Padding(
padding: EdgeInsets.only(
top: 14,
bottom: 14,
right: 12,
),
child: Text('全选'),
),
),
const Spacer(),
FilledButton.tonal(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
if (_favNoteController.checkedCount != 0) {
showConfirmDialog(
context: context,
title: '确定删除已选中的笔记吗?',
onConfirm: _favNoteController.onRemove,
);
}
},
child: const Text('删除'),
),
const SizedBox(width: 16),
],
),
),
),
),
],
),
),
],
);
}

View File

@@ -17,14 +17,9 @@ class FavNoteController
}
@override
void onSelect(FavNoteItemModel item, [bool disableSelect = true]) {
super.onSelect(item, false);
}
@override
void handleSelect([bool checked = false, bool disableSelect = true]) {
void handleSelect({bool checked = false, bool disableSelect = true}) {
allSelected.value = checked;
super.handleSelect(checked, false);
super.handleSelect(checked: checked, disableSelect: disableSelect);
}
@override

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/select_mask.dart';
import 'package:PiliPlus/models_new/fav/fav_note/list.dart';
import 'package:PiliPlus/pages/fav/note/controller.dart';
import 'package:PiliPlus/utils/page_utils.dart';
@@ -64,62 +65,9 @@ class FavNoteItem extends StatelessWidget {
height: boxConstraints.maxHeight,
),
Positioned.fill(
child: IgnorePointer(
child: LayoutBuilder(
builder: (context, constraints) =>
AnimatedOpacity(
opacity: item.checked == true ? 1 : 0,
duration: const Duration(
milliseconds: 200,
),
child: Container(
alignment: Alignment.center,
height: constraints.maxHeight,
width:
constraints.maxHeight *
StyleString.aspectRatio,
decoration: BoxDecoration(
borderRadius: StyleString.mdRadius,
color: Colors.black.withValues(
alpha: 0.6,
),
),
child: SizedBox(
width: 34,
height: 34,
child: AnimatedScale(
scale: item.checked == true ? 1 : 0,
duration: const Duration(
milliseconds: 250,
),
curve: Curves.easeInOut,
child: IconButton(
tooltip: '取消选择',
style: ButtonStyle(
padding:
WidgetStateProperty.all(
EdgeInsets.zero,
),
backgroundColor:
WidgetStatePropertyAll(
theme.colorScheme.surface
.withValues(
alpha: 0.8,
),
),
),
onPressed: null,
icon: Icon(
Icons.done_all_outlined,
color:
theme.colorScheme.primary,
),
),
),
),
),
),
),
child: selectMask(
theme,
item.checked == true,
),
),
],

View File

@@ -37,127 +37,128 @@ class _FavPgcChildPageState extends State<FavPgcChildPage>
super.build(context);
final theme = Theme.of(context);
final padding = MediaQuery.paddingOf(context);
return LayoutBuilder(
builder: (context, constraints) => Stack(
clipBehavior: Clip.none,
children: [
refreshIndicator(
onRefresh: _favPgcController.onRefresh,
child: CustomScrollView(
controller: _favPgcController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(bottom: padding.bottom + 80),
sliver: Obx(
() => _buildBody(_favPgcController.loadingState.value),
),
final bottomH = 50 + padding.bottom;
return Stack(
clipBehavior: Clip.none,
children: [
refreshIndicator(
onRefresh: _favPgcController.onRefresh,
child: CustomScrollView(
controller: _favPgcController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(bottom: padding.bottom + 80),
sliver: Obx(
() => _buildBody(_favPgcController.loadingState.value),
),
],
),
),
],
),
Positioned(
top: constraints.maxHeight,
left: 0,
right: 0,
child: Obx(
() => AnimatedSlide(
offset: _favPgcController.enableMultiSelect.value
? const Offset(0, -1)
: Offset.zero,
duration: const Duration(milliseconds: 150),
child: Container(
padding: padding,
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
border: Border(
top: BorderSide(
width: 0.5,
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
Positioned(
left: 0,
right: 0,
bottom: -bottomH,
child: Obx(
() => AnimatedSlide(
offset: _favPgcController.enableMultiSelect.value
? const Offset(0, -1)
: Offset.zero,
duration: const Duration(milliseconds: 150),
child: Container(
height: bottomH,
padding: padding,
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
border: Border(
top: BorderSide(
width: 0.5,
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 16),
iconButton(
size: 32,
tooltip: '取消',
context: context,
icon: Icons.clear,
onPressed: _favPgcController.onDisable,
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 16),
iconButton(
size: 32,
tooltip: '取消',
context: context,
icon: Icons.clear,
onPressed: _favPgcController.onDisable,
),
const SizedBox(width: 12),
Obx(
() => Checkbox(
value: _favPgcController.allSelected.value,
onChanged: (value) {
_favPgcController.handleSelect(
checked: !_favPgcController.allSelected.value,
);
},
),
const SizedBox(width: 12),
Obx(
() => Checkbox(
value: _favPgcController.allSelected.value,
onChanged: (value) {
_favPgcController.handleSelect(
!_favPgcController.allSelected.value,
);
},
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _favPgcController.handleSelect(
checked: !_favPgcController.allSelected.value,
disableSelect: false,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _favPgcController.handleSelect(
!_favPgcController.allSelected.value,
),
child: const Padding(
padding: EdgeInsets.only(
top: 14,
bottom: 14,
right: 12,
),
child: Text('全选'),
child: const Padding(
padding: EdgeInsets.only(
top: 14,
bottom: 14,
right: 12,
),
child: Text('全选'),
),
const Spacer(),
...const [
(followStatus: 1, title: '想看'),
(followStatus: 2, title: ''),
(followStatus: 3, title: ''),
]
.where(
(item) => item.followStatus != widget.followStatus,
)
.map(
(item) => Padding(
padding: const EdgeInsets.only(left: 25),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_favPgcController.checkedCount != 0) {
_favPgcController.onUpdateList(
item.followStatus,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 5,
),
child: Text(
'标记为${item.title}',
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
const Spacer(),
...const [
(followStatus: 1, title: ''),
(followStatus: 2, title: ''),
(followStatus: 3, title: '看过'),
]
.where(
(item) => item.followStatus != widget.followStatus,
)
.map(
(item) => Padding(
padding: const EdgeInsets.only(left: 25),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_favPgcController.checkedCount != 0) {
_favPgcController.onUpdateList(
item.followStatus,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 5,
),
child: Text(
'标记为${item.title}',
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
),
),
const SizedBox(width: 20),
],
),
),
const SizedBox(width: 20),
],
),
),
),
),
],
),
),
],
);
}

View File

@@ -24,14 +24,9 @@ class FavPgcController
}
@override
void onSelect(FavPgcItemModel item, [bool disableSelect = true]) {
super.onSelect(item, false);
}
@override
void handleSelect([bool checked = false, bool disableSelect = true]) {
void handleSelect({bool checked = false, bool disableSelect = true}) {
allSelected.value = checked;
super.handleSelect(checked, false);
super.handleSelect(checked: checked, disableSelect: disableSelect);
}
@override
@@ -88,10 +83,10 @@ class FavPgcController
..refresh();
ctr.allSelected.value = false;
}
afterDelete(removeList);
} catch (e) {
if (kDebugMode) debugPrint('fav pgc onUpdate: $e');
}
afterDelete(removeList);
}
SmartDialog.showToast(res['msg']);
}

View File

@@ -2,6 +2,7 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/select_mask.dart';
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models_new/fav/fav_pgc/list.dart';
import 'package:PiliPlus/pages/common/multi_select/base.dart';
@@ -81,71 +82,11 @@ class FavPgcItem extends StatelessWidget {
),
),
Positioned.fill(
child: IgnorePointer(
child: LayoutBuilder(
builder: (context, constraints) =>
AnimatedOpacity(
opacity: item.checked == true
? 1
: 0,
duration: const Duration(
milliseconds: 200,
),
child: Container(
alignment: Alignment.center,
height: constraints.maxHeight,
width:
constraints.maxHeight *
StyleString.aspectRatio,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(
Radius.circular(4),
),
color: Colors.black.withValues(
alpha: 0.6,
),
),
child: SizedBox(
width: 34,
height: 34,
child: AnimatedScale(
scale: item.checked == true
? 1
: 0,
duration: const Duration(
milliseconds: 250,
),
curve: Curves.easeInOut,
child: IconButton(
tooltip: '取消选择',
style: ButtonStyle(
padding:
WidgetStateProperty.all(
EdgeInsets.zero,
),
backgroundColor:
WidgetStatePropertyAll(
theme
.colorScheme
.surface
.withValues(
alpha: 0.8,
),
),
),
onPressed: null,
icon: Icon(
Icons.done_all_outlined,
color: theme
.colorScheme
.primary,
),
),
),
),
),
),
child: selectMask(
theme,
item.checked == true,
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
),
),

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -36,7 +35,7 @@ class _FavVideoPageState extends State<FavVideoPage>
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
bottom: 80 + MediaQuery.paddingOf(context).bottom,
),
sliver: Obx(

View File

@@ -246,20 +246,16 @@ class _CreateFavPageState extends State<CreateFavPage> {
if (_cover?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: LayoutBuilder(
builder: (context, constraints) {
return ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(6),
),
child: CachedNetworkImage(
imageUrl: ImageUtil.thumbnailUrl(_cover!),
height: constraints.maxHeight,
width: constraints.maxHeight * 16 / 9,
fit: BoxFit.cover,
),
);
},
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(6),
),
child: CachedNetworkImage(
imageUrl: ImageUtil.thumbnailUrl(_cover!),
height: 55,
width: 88,
fit: BoxFit.cover,
),
),
),
Icon(

View File

@@ -279,7 +279,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => _favDetailController.handleSelect(true),
onPressed: () => _favDetailController.handleSelect(checked: true),
child: const Text('全选'),
),
TextButton(

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/appbar/appbar.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
@@ -60,7 +59,7 @@ class _HistoryPageState extends State<HistoryPage>
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -138,7 +137,7 @@ class _HotPageState extends CommonPageState<HotPage, HotController>
),
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(

View File

@@ -52,7 +52,7 @@ class _MemberCoinArcPageState extends State<MemberCoinArcPage> {
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
left: StyleString.safeSpace,
right: StyleString.safeSpace,
bottom: MediaQuery.paddingOf(context).bottom + 80,

View File

@@ -1,10 +1,8 @@
import 'package:PiliPlus/common/constants.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/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/dynamics_tab/view.dart';
import 'package:PiliPlus/pages/member_dynamics/controller.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/grid.dart';
@@ -25,7 +23,7 @@ class MemberDynamicsPage extends StatefulWidget {
}
class _MemberDynamicsPageState extends State<MemberDynamicsPage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, DynMixin {
late MemberDynamicsController _memberDynamicController;
late int mid;
@@ -74,23 +72,14 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage>
),
);
late double _maxWidth;
Widget _buildContent(LoadingState<List<DynamicItemModel>?> loadingState) {
return switch (loadingState) {
Loading() => DynamicsTabPage.dynSkeleton(
GlobalData().dynamicsWaterfallFlow,
),
Loading() => dynSkeleton,
Success(:var response) =>
response?.isNotEmpty == true
? GlobalData().dynamicsWaterfallFlow
? SliverWaterfallFlow(
gridDelegate:
SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
crossAxisSpacing: StyleString.cardSpace / 2,
callback: (value) => _maxWidth = value,
),
gridDelegate: gridDelegate,
delegate: SliverChildBuilderDelegate(
(_, index) {
if (index == response.length - 1) {
@@ -100,7 +89,7 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage>
item: response[index],
onRemove: _memberDynamicController.onRemove,
onSetTop: _memberDynamicController.onSetTop,
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
},
childCount: response!.length,
@@ -120,7 +109,7 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage>
item: response[index],
onRemove: _memberDynamicController.onRemove,
onSetTop: _memberDynamicController.onSetTop,
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
},
itemCount: response!.length,

View File

@@ -56,34 +56,27 @@ class MemberFavItem extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: item.cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
),
if (item.type == 21)
const PBadge(
right: 6,
top: 6,
text: '合集',
)
else if (item.type == 11)
const PBadge(
right: 6,
top: 6,
text: '收藏夹',
),
],
);
},
),
Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: item.cover,
width: 140.8,
height: 88,
),
if (item.type == 21)
const PBadge(
right: 6,
top: 6,
text: '合集',
)
else if (item.type == 11)
const PBadge(
right: 6,
top: 6,
text: '收藏夹',
),
],
),
const SizedBox(width: 10),
Expanded(

View File

@@ -37,17 +37,10 @@ class MemberFavItem extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return NetworkImgLayer(
src: item.cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
);
},
),
NetworkImgLayer(
src: item.cover,
width: 140.8,
height: 88,
),
const SizedBox(width: 10),
Expanded(

View File

@@ -52,7 +52,7 @@ class _MemberLikeArcPageState extends State<MemberLikeArcPage> {
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
left: StyleString.safeSpace,
right: StyleString.safeSpace,
bottom: MediaQuery.paddingOf(context).bottom + 80,

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -6,7 +5,6 @@ import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/member/search_type.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/dynamics_tab/view.dart';
import 'package:PiliPlus/pages/member_search/child/controller.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/grid.dart';
@@ -31,7 +29,7 @@ class MemberSearchChildPage extends StatefulWidget {
}
class _MemberSearchChildPageState extends State<MemberSearchChildPage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, DynMixin {
MemberSearchChildController get _controller => widget.controller;
@override
@@ -66,14 +64,10 @@ class _MemberSearchChildPageState extends State<MemberSearchChildPage>
childCount: 10,
),
),
MemberSearchType.dynamic => DynamicsTabPage.dynSkeleton(
GlobalData().dynamicsWaterfallFlow,
),
MemberSearchType.dynamic => dynSkeleton,
};
}
late double _maxWidth;
Widget _buildBody(LoadingState<List?> loadingState) {
return switch (loadingState) {
Loading() => _buildLoading,
@@ -99,12 +93,7 @@ class _MemberSearchChildPageState extends State<MemberSearchChildPage>
MemberSearchType.dynamic =>
GlobalData().dynamicsWaterfallFlow
? SliverWaterfallFlow(
gridDelegate:
SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
crossAxisSpacing: StyleString.safeSpace,
callback: (value) => _maxWidth = value,
),
gridDelegate: gridDelegate,
delegate: SliverChildBuilderDelegate(
(_, index) {
if (index == response.length - 1) {
@@ -112,7 +101,7 @@ class _MemberSearchChildPageState extends State<MemberSearchChildPage>
}
return DynamicPanel(
item: response[index],
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
},
childCount: response!.length,
@@ -130,7 +119,7 @@ class _MemberSearchChildPageState extends State<MemberSearchChildPage>
}
return DynamicPanel(
item: response[index],
maxWidth: _maxWidth,
maxWidth: maxWidth,
);
},
itemCount: response!.length,

View File

@@ -54,17 +54,13 @@ class FavFolderItem extends StatelessWidget {
),
],
),
child: LayoutBuilder(
builder: (context, box) {
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: item.cover,
width: box.maxWidth,
height: box.maxHeight,
),
);
},
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
src: item.cover,
width: 176,
height: 110,
),
),
),
Text(

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -44,7 +43,7 @@ class _ZonePageState extends CommonPageState<ZonePage, ZoneController>
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(() => _buildBody(controller.loadingState.value)),

View File

@@ -115,11 +115,9 @@ class _RcmdPageState extends CommonPageState<RcmdPage, RcmdController>
} else {
return VideoCardV(
videoItem: response[index],
onRemove: () {
controller.loadingState
..value.data!.removeAt(index)
..refresh();
},
onRemove: () => controller.loadingState
..value.data!.removeAt(index)
..refresh(),
);
}
},

View File

@@ -423,7 +423,7 @@ class PgcIntroController extends CommonIntroController {
SmartDialog.showToast('已三连');
return;
}
var result = await VideoHttp.triple(epId: epId, seasonId: seasonId);
var result = await VideoHttp.pgcTriple(epId: epId, seasonId: seasonId);
if (result['status']) {
PgcTriple data = result['data'];
late final stat = pgcItem.stat!;
@@ -431,7 +431,8 @@ class PgcIntroController extends CommonIntroController {
stat.like++;
hasLike.value = true;
}
if ((data.coin == 1) != hasCoin) {
final hasCoin = data.coin == 1;
if (this.hasCoin != hasCoin) {
stat.coin += 2;
coinNum.value = 2;
GlobalData().afterCoin(2);
@@ -440,7 +441,11 @@ class PgcIntroController extends CommonIntroController {
stat.favorite++;
hasFav.value = true;
}
SmartDialog.showToast('三连成功');
if (!hasCoin) {
SmartDialog.showToast('投币失败');
} else {
SmartDialog.showToast('三连成功');
}
} else {
SmartDialog.showToast(result['msg']);
}

View File

@@ -113,7 +113,7 @@ class _PgcIntroPageState extends TripleState<PgcIntroPage>
Widget? _buildBreif(PgcInfoModel item) {
final img = item.brief?.img;
if (img != null && img.isNotEmpty) {
final maxWidth = widget.maxWidth - 2 * StyleString.safeSpace;
final maxWidth = widget.maxWidth - 24;
double padding = max(0, maxWidth - 400);
final imgWidth = maxWidth - padding;
padding = padding / 2;

View File

@@ -193,7 +193,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
SmartDialog.showToast('已三连');
return;
}
var result = await VideoHttp.oneThree(bvid: bvid);
var result = await VideoHttp.ugcTriple(bvid: bvid);
if (result['status']) {
UgcTriple data = result['data'];
late final stat = videoDetail.value.stat!;
@@ -211,7 +211,11 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
hasFav.value = true;
}
hasDislike.value = false;
SmartDialog.showToast('三连成功');
if (data.coin != true) {
SmartDialog.showToast('投币失败');
} else {
SmartDialog.showToast('三连成功');
}
} else {
SmartDialog.showToast(result['msg']);
}

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
@@ -51,7 +50,6 @@ class MediaListPanel extends CommonCollapseSlidePage {
class _MediaListPanelState
extends CommonCollapseSlidePageState<MediaListPanel> {
late final int _index;
late final RxBool desc = widget.desc.obs;
@override
void initState() {
@@ -74,19 +72,14 @@ class _MediaListPanelState
title: Text(widget.panelTitle ?? '稍后再看'),
backgroundColor: Colors.transparent,
actions: [
Obx(
() {
final desc = this.desc.value;
return mediumButton(
tooltip: desc ? '顺序播放' : '倒序播放',
icon: desc
? MdiIcons.sortAscending
: MdiIcons.sortDescending,
onPressed: () {
widget.onReverse();
this.desc.value = !desc;
},
);
mediumButton(
tooltip: widget.desc ? '顺序播放' : '倒序播放',
icon: widget.desc
? MdiIcons.sortAscending
: MdiIcons.sortDescending,
onPressed: () {
Get.back();
widget.onReverse();
},
),
mediumButton(
@@ -131,169 +124,170 @@ class _MediaListPanelState
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
itemBuilder: ((context, index) {
var item = widget.mediaList[index];
if (index == widget.mediaList.length - 1 &&
(widget.count == null ||
widget.mediaList.length < widget.count!)) {
widget.loadMoreMedia();
}
var item = widget.mediaList[index];
final isCurr = item.bvid == widget.getBvId();
return SizedBox(
height: 98,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () {
if (item.type != 2) {
SmartDialog.showToast('不支持播放该类型视频');
return;
}
Get.back();
widget.onChangeEpisode(item);
},
onLongPress: () => imageSaveDialog(
title: item.title,
cover: item.cover,
aid: item.aid,
bvid: item.bvid,
),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: item.cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
),
if (item.badge?.isNotEmpty == true)
PBadge(
text: item.badge,
right: 6.0,
top: 6.0,
type: switch (item.badge) {
'充电专属' => PBadgeType.error,
_ => PBadgeType.primary,
},
),
PBadge(
text: DurationUtil.formatDuration(
item.duration,
),
right: 6.0,
bottom: 6.0,
type: PBadgeType.gray,
),
],
);
},
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: isCurr ? FontWeight.bold : null,
color: isCurr
? theme.colorScheme.primary
: null,
),
),
if (item.type == 24 &&
item.intro?.isNotEmpty == true) ...[
const SizedBox(height: 3),
Text(
item.intro!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
],
const Spacer(),
Text(
item.upper!.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
if (item.type == 2) ...[
const SizedBox(height: 3),
Row(
spacing: 8,
children: [
StatWidget(
type: StatType.play,
value: item.cntInfo!.play,
),
StatWidget(
type: StatType.danmaku,
value: item.cntInfo!.danmaku,
),
],
),
],
],
),
),
],
),
),
if (showDelBtn && !isCurr)
Positioned(
right: 12,
bottom: -6,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => showConfirmDialog(
context: context,
title: '确定移除该视频?',
onConfirm: () => widget.onDelete!(item, index),
),
onLongPress: () => widget.onDelete!(item, index),
child: Padding(
padding: const EdgeInsets.all(9),
child: Icon(
Icons.clear,
size: 18,
color: theme.colorScheme.outline,
),
),
),
),
],
),
),
),
);
return _buildItem(theme, index, item, isCurr, showDelBtn);
}),
separatorBuilder: (context, index) => const SizedBox(height: 2),
);
},
);
Widget _buildItem(
ThemeData theme,
int index,
MediaListItemModel item,
bool isCurr,
bool showDelBtn,
) {
return SizedBox(
height: 98,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () {
if (item.type != 2) {
SmartDialog.showToast('不支持播放该类型视频');
return;
}
Get.back();
widget.onChangeEpisode(item);
},
onLongPress: () => imageSaveDialog(
title: item.title,
cover: item.cover,
aid: item.aid,
bvid: item.bvid,
),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: item.cover,
width: 140.8,
height: 88,
),
if (item.badge?.isNotEmpty == true)
PBadge(
text: item.badge,
right: 6.0,
top: 6.0,
type: switch (item.badge) {
'充电专属' => PBadgeType.error,
_ => PBadgeType.primary,
},
),
PBadge(
text: DurationUtil.formatDuration(
item.duration,
),
right: 6.0,
bottom: 6.0,
type: PBadgeType.gray,
),
],
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: isCurr ? FontWeight.bold : null,
color: isCurr ? theme.colorScheme.primary : null,
),
),
if (item.type == 24 &&
item.intro?.isNotEmpty == true) ...[
const SizedBox(height: 3),
Text(
item.intro!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
],
const Spacer(),
Text(
item.upper!.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
if (item.type == 2) ...[
const SizedBox(height: 3),
Row(
spacing: 8,
children: [
StatWidget(
type: StatType.play,
value: item.cntInfo!.play,
),
StatWidget(
type: StatType.danmaku,
value: item.cntInfo!.danmaku,
),
],
),
],
],
),
),
],
),
),
if (showDelBtn && !isCurr)
Positioned(
right: 12,
bottom: -6,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => showConfirmDialog(
context: context,
title: '确定移除该视频?',
onConfirm: () => widget.onDelete!(item, index),
),
onLongPress: () => widget.onDelete!(item, index),
child: Padding(
padding: const EdgeInsets.all(9),
child: Icon(
Icons.clear,
size: 18,
color: theme.colorScheme.outline,
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -208,20 +208,16 @@ class _PayCoinsPageState extends State<PayCoinsPage>
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
bool isV = constraints.maxHeight > constraints.maxWidth;
return isV
? _buildBody(isV)
: Row(
children: [
const Spacer(),
Expanded(flex: 3, child: _buildBody(isV)),
const Spacer(),
],
);
},
);
bool isPortrait = context.isPortrait;
return isPortrait
? _buildBody(isPortrait)
: Row(
children: [
const Spacer(),
Expanded(flex: 3, child: _buildBody(isPortrait)),
const Spacer(),
],
);
}
Widget _buildBody(bool isV) => Stack(

View File

@@ -1,4 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
@@ -31,7 +30,7 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel>
super.build(context);
return SliverPadding(
padding: const EdgeInsets.only(
top: StyleString.safeSpace - 5,
top: 7,
bottom: 80,
),
sliver: Obx(() => _buildBody(_relatedController.loadingState.value)),

View File

@@ -50,7 +50,7 @@ class _ViewPointsPageState
toolbarHeight: 45,
actions: [
const Text(
'分段进度条',
'分段进度条 ',
style: TextStyle(fontSize: 16),
),
Obx(
@@ -116,63 +116,71 @@ class _ViewPointsPageState
currentIndex = index;
}
}
return ListTile(
dense: true,
onTap: segment.from != null
? () {
currentIndex = index;
plPlayerController?.danmakuController?.clear();
plPlayerController?.videoPlayerController?.seek(
Duration(seconds: segment.from!),
);
Get.back();
}
: null,
leading: segment.url?.isNotEmpty == true
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: currentIndex == index
? BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(6),
),
border: Border.all(
width: 1.8,
strokeAlign: BorderSide.strokeAlignOutside,
color: theme.colorScheme.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) => NetworkImgLayer(
radius: 6,
src: segment.url,
width: constraints.maxHeight * StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: null,
title: Text(
segment.title ?? '',
style: TextStyle(
fontSize: 14,
fontWeight: currentIndex == index ? FontWeight.bold : null,
color: currentIndex == index ? theme.colorScheme.primary : null,
),
),
subtitle: Text(
'${segment.from != null ? DurationUtil.formatDuration(segment.from) : ''} - ${segment.to != null ? DurationUtil.formatDuration(segment.to) : ''}',
style: TextStyle(
fontSize: 13,
color: currentIndex == index
? theme.colorScheme.primary
: theme.colorScheme.outline,
),
),
);
final isCurr = currentIndex == index;
return _buildItem(theme, segment, isCurr);
},
separatorBuilder: (context, index) => divider,
);
}
Widget _buildItem(ThemeData theme, Segment segment, bool isCurr) {
final theme = Theme.of(context);
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: segment.from != null
? () {
Get.back();
plPlayerController
?..danmakuController?.clear()
..videoPlayerController?.seek(
Duration(seconds: segment.from!),
);
}
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
vertical: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NetworkImgLayer(
src: segment.url,
width: 140.8,
height: 88,
),
const SizedBox(width: 10),
Expanded(
child: Column(
spacing: 10,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
segment.title ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: isCurr
? TextStyle(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
)
: null,
),
Text(
'${segment.from != null ? DurationUtil.formatDuration(segment.from) : ''} - '
'${segment.to != null ? DurationUtil.formatDuration(segment.to) : ''}',
style: TextStyle(color: theme.colorScheme.outline),
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -450,7 +450,7 @@ class RequestUtils {
mid: isCopy ? mid : null,
).then((res) {
if (res.isSuccess) {
ctr.handleSelect(false);
ctr.handleSelect(checked: false);
if (!isCopy) {
ctr.loadingState
..value.data!.removeWhere(removeList.contains)

View File

@@ -1,8 +1,55 @@
import 'package:flutter/material.dart' show ValueChanged;
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/dynamic_card.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show SliverConstraints;
import 'package:waterfall_flow/waterfall_flow.dart'
show SliverWaterfallFlowDelegate;
mixin DynMixin {
late double maxWidth;
late final gridDelegate = SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
crossAxisSpacing: 4,
callback: (value) => maxWidth = value,
);
late final skeDelegate = SliverGridDelegateWithExtentAndRatio(
crossAxisSpacing: 4,
mainAxisSpacing: 4,
maxCrossAxisExtent: Grid.smallCardWidth * 2,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent: 50,
);
Widget get dynSkeleton {
if (!GlobalData().dynamicsWaterfallFlow) {
return SliverCrossAxisGroup(
slivers: [
const SliverFillRemaining(),
SliverConstrainedCrossAxis(
maxExtent: Grid.smallCardWidth * 2,
sliver: SliverList.builder(
itemBuilder: (_, _) => const DynamicCardSkeleton(),
itemCount: 10,
),
),
const SliverFillRemaining(),
],
);
}
return SliverGrid(
gridDelegate: skeDelegate,
delegate: SliverChildBuilderDelegate(
(_, _) => const DynamicCardSkeleton(),
childCount: 10,
),
);
}
}
class SliverWaterfallFlowDelegateWithMaxCrossAxisExtent
extends SliverWaterfallFlowDelegate {
/// Creates a delegate that makes masonry layouts with tiles that have a maximum