Compare commits

..

5 Commits

Author SHA1 Message Date
53b72bec25 mod: read schame补充 2024-03-24 13:16:13 +08:00
20745a4541 Merge branch 'main' into feature-appScheme 2024-03-24 11:44:02 +08:00
4db5a950f3 mod: 弹幕开关状态 2024-03-24 11:43:44 +08:00
c9327c97e5 fix: 评论投票message重复 2024-03-24 11:43:44 +08:00
8ff387d54a mod: 评论头部样式 2024-03-24 11:43:44 +08:00
12 changed files with 182 additions and 274 deletions

View File

@ -36,7 +36,7 @@ class NetworkImgLayer extends StatelessWidget {
final int defaultImgQuality = GlobalData().imgQuality;
final String imageUrl =
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp';
print(imageUrl);
// print(imageUrl);
int? memCacheWidth, memCacheHeight;
double aspectRatio = (width / height).toDouble();

View File

@ -1,4 +1,3 @@
import 'package:expandable/expandable.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@ -16,7 +15,6 @@ import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import '../../../../http/user.dart';
import 'widgets/action_item.dart';
import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
@ -139,9 +137,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
late String memberHeroTag;
late bool enableAi;
bool isProcessing = false;
RxBool isExpand = false.obs;
late ExpandableController _expandableCtr;
void Function()? handleState(Future Function() action) {
return isProcessing
? null
@ -165,7 +160,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
follower = Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
}
// 收藏
@ -218,8 +212,13 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 视频介绍
showIntroDetail() {
feedBack();
isExpand.value = !(isExpand.value);
_expandableCtr.toggle();
showBottomSheet(
context: context,
enableDrag: true,
builder: (BuildContext context) {
return IntroDetail(videoDetail: widget.videoDetail!);
},
);
}
// 用户主页
@ -243,12 +242,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
);
}
@override
void dispose() {
_expandableCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData t = Theme.of(context);
@ -266,34 +259,14 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: ExpandablePanel(
controller: _expandableCtr,
collapsed: Text(
widget.videoDetail!.title!,
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
expanded: Text(
widget.videoDetail!.title!,
softWrap: true,
maxLines: 4,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
child: Text(
widget.videoDetail!.title!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Stack(
@ -357,20 +330,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
],
),
/// 视频简介
ExpandablePanel(
controller: _expandableCtr,
collapsed: const SizedBox(height: 0),
expanded: IntroDetail(videoDetail: widget.videoDetail!),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
),
),
/// 点赞收藏转发
actionGrid(context, videoIntroController),
// 合集
@ -479,7 +438,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
margin: const EdgeInsets.only(top: 6, bottom: 4),
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
primary: false,
padding: EdgeInsets.zero,
crossAxisCount: 5,
@ -493,6 +451,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
selectStatus: videoIntroController.hasLike.value,
text: widget.videoDetail!.stat!.like!.toString()),
),
// ActionItem(
// icon: const Icon(FontAwesomeIcons.clock),
// onTap: () => videoIntroController.actionShareVideo(),
// selectStatus: false,
// loadingStatus: loadingStatus,
// text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
@ -513,14 +477,10 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () async {
final res =
await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid);
SmartDialog.showToast(res['msg']);
},
icon: const Icon(FontAwesomeIcons.comment),
onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false,
text: '稍后看',
text: widget.videoDetail!.stat!.reply!.toString(),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),

View File

@ -1,10 +1,16 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
Box localCache = GStrorage.localCache;
late double sheetHeight;
class IntroDetail extends StatelessWidget {
const IntroDetail({
super.key,
@ -14,39 +20,105 @@ class IntroDetail extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.background,
padding: EdgeInsets.only(
left: 14,
right: 14,
bottom: MediaQuery.of(context).padding.bottom + 20),
height: sheetHeight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 4),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));
SmartDialog.showToast('已复制');
},
child: Text(
videoDetail!.bvid!,
style: TextStyle(
fontSize: 13, color: Theme.of(context).colorScheme.primary),
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
const SizedBox(height: 4),
Text.rich(
style: const TextStyle(height: 1.4),
TextSpan(
children: [
buildContent(context, videoDetail!),
],
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
videoDetail!.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Row(
children: [
StatView(
theme: 'gray',
view: videoDetail!.stat!.view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: videoDetail!.stat!.danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(videoDetail!.pubdate,
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
videoDetail!.bvid!,
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 4),
Text.rich(
style: const TextStyle(
height: 1.4,
// fontSize: 13,
),
TextSpan(
children: [
buildContent(context, videoDetail!),
],
),
),
],
),
),
),
],
),
),
),
)
],
),
),
);
));
}
InlineSpan buildContent(BuildContext context, content) {

View File

@ -148,35 +148,14 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
floating: true,
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
height: 45,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.1)),
),
),
height: 40,
padding: const EdgeInsets.fromLTRB(12, 6, 6, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'${_videoReplyController.count.value}条回复',
key: ValueKey<int>(
_videoReplyController.count.value),
),
),
Text(
'${_videoReplyController.sortTypeLabel.value}评论',
style: const TextStyle(fontSize: 13),
),
SizedBox(
height: 35,
@ -184,10 +163,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
onPressed: () =>
_videoReplyController.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_videoReplyController.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
label: Obx(
() => Text(
_videoReplyController.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
),
),
),
)
],
@ -329,8 +310,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_MySliverPersistentHeaderDelegate({required this.child});
final double _minExtent = 45;
final double _maxExtent = 45;
final double _minExtent = 40;
final double _maxExtent = 40;
final Widget child;
@override

View File

@ -498,7 +498,7 @@ InlineSpan buildContent(
return str;
});
}
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')

View File

@ -372,9 +372,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
false)
? SvgPicture.asset(
'assets/images/video/danmu_close.svg',
// ignore: deprecated_member_use
color:
Theme.of(context).colorScheme.outline,
)
: SvgPicture.asset(
'assets/images/video/danmu_open.svg',

View File

@ -17,16 +17,12 @@ class ScrollAppBar extends StatelessWidget {
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
final videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
double scrollDistance = scrollVal;
if (scrollVal > videoHeight - kToolbarHeight) {
scrollDistance = videoHeight - kToolbarHeight;
}
return Positioned(
top: -videoHeight + scrollDistance + kToolbarHeight + 0.5,
top: -videoHeight + scrollVal + kToolbarHeight + 0.5,
left: 0,
right: 0,
child: Opacity(
opacity: scrollDistance / (videoHeight - kToolbarHeight),
opacity: scrollVal / (videoHeight - kToolbarHeight),
child: Container(
height: statusBarHeight + kToolbarHeight,
color: Theme.of(context).colorScheme.background,

View File

@ -32,14 +32,28 @@ class _ExpandedSectionState extends State<ExpandedSection>
_runExpandCheck();
}
///Setting up the animation
// void prepareAnimations() {
// expandController = AnimationController(
// vsync: this, duration: const Duration(milliseconds: 500));
// animation = CurvedAnimation(
// parent: expandController,
// curve: Curves.fastOutSlowIn,
// );
// }
void prepareAnimations() {
expandController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 400));
Animation<double> curve = CurvedAnimation(
parent: expandController,
curve: Curves.linear,
curve: Curves.fastOutSlowIn,
);
animation = Tween(begin: widget.begin, end: widget.end).animate(curve);
// animation = CurvedAnimation(
// parent: expandController,
// curve: Curves.fastOutSlowIn,
// );
}
void _runExpandCheck() {
@ -53,9 +67,7 @@ class _ExpandedSectionState extends State<ExpandedSection>
@override
void didUpdateWidget(ExpandedSection oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.expand != oldWidget.expand) {
_runExpandCheck();
}
_runExpandCheck();
}
@override

View File

@ -17,7 +17,6 @@ import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../http/danmaku.dart';
import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart';
import '../introduction/index.dart';
@ -53,7 +52,7 @@ class _HeaderControlState extends State<HeaderControl> {
final Box<dynamic> videoStorage = GStrorage.video;
late List<double> speedsList;
double buttonSpace = 8;
RxBool isFullScreen = false.obs;
bool showTitle = false;
late String heroTag;
late VideoIntroController videoIntroController;
late VideoDetailData videoDetail;
@ -70,8 +69,13 @@ class _HeaderControlState extends State<HeaderControl> {
}
void fullScreenStatusListener() {
widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) {
isFullScreen.value = val;
widget.videoDetailCtr!.plPlayerController.isFullScreen
.listen((bool isFullScreen) {
if (isFullScreen) {
showTitle = true;
} else {
showTitle = false;
}
/// TODO setState() called after dispose()
if (mounted) {
@ -214,87 +218,6 @@ class _HeaderControlState extends State<HeaderControl> {
);
}
/// 发送弹幕
void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController();
bool isSending = false; // 追踪是否正在发送
showDialog(
context: Get.context!,
builder: (BuildContext context) {
// TODO: 支持更多类型和颜色的弹幕
return AlertDialog(
title: const Text('发送弹幕(测试)'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextField(
controller: textController,
);
}),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextButton(
onPressed: isSending
? null
: () async {
final String msg = textController.text;
if (msg.isEmpty) {
SmartDialog.showToast('弹幕内容不能为空');
return;
} else if (msg.length > 100) {
SmartDialog.showToast('弹幕内容不能超过100个字符');
return;
}
setState(() {
isSending = true; // 开始发送,更新状态
});
//修改按钮文字
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: widget.videoDetailCtr!.cid.value,
msg: textController.text,
bvid: widget.videoDetailCtr!.bvid,
progress:
widget.controller!.position.value.inMilliseconds,
type: 1,
);
setState(() {
isSending = false; // 发送结束,更新状态
});
if (res['status']) {
SmartDialog.showToast('发送成功');
// 发送成功,自动预览该弹幕,避免重新请求
// TODO: 暂停状态下预览弹幕仍会移动与计时可考虑添加到dmSegList或其他方式实现
widget.controller!.danmakuController!.addItems([
DanmakuItem(
msg,
color: Colors.white,
time: widget
.controller!.position.value.inMilliseconds,
type: DanmakuItemType.scroll,
isSend: true,
)
]);
Get.back();
} else {
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
}
},
child: Text(isSending ? '发送中...' : '发送'),
);
})
],
);
},
);
}
/// 定时关闭
void scheduleExit() async {
const List<int> scheduleTimeChoices = [
@ -1106,7 +1029,7 @@ class _HeaderControlState extends State<HeaderControl> {
},
),
SizedBox(width: buttonSpace),
if (isFullScreen.value &&
if (showTitle &&
isLandscape &&
widget.videoType == SearchType.video) ...[
Column(
@ -1158,43 +1081,6 @@ class _HeaderControlState extends State<HeaderControl> {
// ),
// fuc: () => _.screenshot(),
// ),
if (isFullScreen.value) ...[
SizedBox(
width: 56,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => showShootDanmakuSheet(),
child: const Text(
'发弹幕',
style: textStyle,
),
),
),
SizedBox(
width: 34,
height: 34,
child: Obx(
() => IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
_.isOpenDanmu.value = !_.isOpenDanmu.value;
},
icon: Icon(
_.isOpenDanmu.value
? Icons.subtitles_outlined
: Icons.subtitles_off_outlined,
size: 19,
color: Colors.white,
),
),
),
),
],
SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...<Widget>[
SizedBox(

View File

@ -20,7 +20,7 @@ class PiliSchame {
/// 完整链接进入 b23.无效
appScheme.getLatestScheme().then((SchemeEntity? value) {
if (value != null) {
_fullPathPush(value);
_routePush(value);
}
});
@ -37,7 +37,6 @@ class PiliSchame {
final String scheme = value.scheme;
final String host = value.host;
final String path = value.path;
if (scheme == 'bilibili') {
if (host == 'root') {
Navigator.popUntil(
@ -85,6 +84,14 @@ class PiliSchame {
}
} else if (host == 'search') {
Get.toNamed('/searchResult', parameters: {'keyword': ''});
} else if (host == 'article') {
final String id = path.split('/').last.split('?').first;
Get.toNamed('/htmlRender', parameters: {
'url': 'https://www.bilibili.com/read/cv$id',
'title': 'cv$id',
'id': 'cv$id',
'dynamicType': 'read'
});
}
}
if (scheme == 'https') {
@ -226,6 +233,13 @@ class PiliSchame {
break;
case 'read':
print('专栏');
String id = 'cv${matchNum(query!['id']!).first}';
Get.toNamed('/htmlRender', parameters: {
'url': value.dataString!,
'title': '',
'id': id,
'dynamicType': 'read'
});
break;
case 'space':
print('个人空间');

View File

@ -433,14 +433,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.3"
expandable:
dependency: "direct main"
description:
name: expandable
sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.1"
extended_image:
dependency: "direct main"
description:

View File

@ -142,8 +142,6 @@ dependencies:
path: 1.8.3
# 电池优化
disable_battery_optimization: ^1.1.1
# 展开/收起
expandable: ^5.0.1
dev_dependencies:
flutter_test: