Compare commits

..

1 Commits

Author SHA1 Message Date
ed8443ba02 feat: navigation Bar编辑 2024-03-27 22:26:47 +08:00
30 changed files with 255 additions and 474 deletions

View File

@ -26,13 +26,13 @@
Xcode 13.4 不支持**auto_orientation**,请注释相关代码 Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```bash ```bash
[] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale [] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale
zh-Hans-CN) zh-Hans-CN)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Xcode - develop for iOS and macOS (Xcode 15.1) [] Xcode - develop for iOS and macOS (Xcode 15.1)
[] Chrome - develop for the web [] Chrome - develop for the web
[] Android Studio (version 2022.3) [] Android Studio (version 2022.3)
[] VS Code (version 1.87.2) [] VS Code (version 1.85.1)
[] Connected device (3 available) [] Connected device (3 available)
[] Network resources [] Network resources
@ -44,9 +44,6 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
## 技术交流 ## 技术交流
Telegram: https://t.me/+lm_oOVmF0RJiODk1 Telegram: https://t.me/+lm_oOVmF0RJiODk1
Tg Beta版本@PiliPala_Beta
QQ频道: https://pd.qq.com/s/365esodk3 QQ频道: https://pd.qq.com/s/365esodk3

View File

@ -1,29 +0,0 @@
## 1.0.22
明天晚上或者后天晚上应该会更新
### 功能
+ 字幕
+ 全屏时选集
+ 动态转发
+ 评论视频并转发
+ 收藏夹删除
+ 合集显示封面
+ 底部导航栏编辑、排序功能
+ 历史记录进度条展示
+ 直播画质切换
+ 排行榜功能
+ 视频详情页推荐视频开关
+ 显示联合投稿up
### 修复
+ 收藏夹个数错误
+ 封面保存权限问题
+ 合集最后1p未展示
+ up主页关注按钮触发灰屏
### 优化
+ 视频简介查看逻辑
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -506,6 +506,4 @@ class Api {
/// 排行榜 /// 排行榜
static const String getRankApi = "/x/web-interface/ranking/v2"; static const String getRankApi = "/x/web-interface/ranking/v2";
/// 取消订阅
static const String cancelSub = '/x/v3/fav/season/unfav';
} }

View File

@ -349,21 +349,4 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
// 取消订阅
static Future cancelSub({required int seasonId}) async {
var res = await Request().post(
Api.cancelSub,
queryParameters: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
} }

View File

@ -34,7 +34,6 @@ void main() async {
.then((_) async { .then((_) async {
await GStrorage.init(); await GStrorage.init();
await setupServiceLocator(); await setupServiceLocator();
clearLogs();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
RecommendFilter(); RecommendFilter();

View File

@ -1,5 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../pages/dynamics/index.dart';
import '../../pages/home/index.dart';
import '../../pages/media/index.dart';
import '../../pages/rank/index.dart';
List defaultNavigationBars = [ List defaultNavigationBars = [
{ {
'id': 0, 'id': 0,
@ -13,6 +18,7 @@ List defaultNavigationBars = [
), ),
'label': "首页", 'label': "首页",
'count': 0, 'count': 0,
'page': const HomePage(),
}, },
{ {
'id': 1, 'id': 1,
@ -26,6 +32,7 @@ List defaultNavigationBars = [
), ),
'label': "排行榜", 'label': "排行榜",
'count': 0, 'count': 0,
'page': const RankPage(),
}, },
{ {
'id': 2, 'id': 2,
@ -39,6 +46,7 @@ List defaultNavigationBars = [
), ),
'label': "动态", 'label': "动态",
'count': 0, 'count': 0,
'page': const DynamicsPage(),
}, },
{ {
'id': 3, 'id': 3,
@ -52,5 +60,6 @@ List defaultNavigationBars = [
), ),
'label': "媒体库", 'label': "媒体库",
'count': 0, 'count': 0,
'page': const MediaPage(),
} }
]; ];

View File

@ -47,23 +47,18 @@ class Vip {
this.status, this.status,
this.dueDate, this.dueDate,
this.label, this.label,
this.nicknameColor,
}); });
int? type; int? type;
int? status; int? status;
int? dueDate; int? dueDate;
Map? label; Map? label;
int? nicknameColor;
Vip.fromJson(Map<String, dynamic> json) { Vip.fromJson(Map<String, dynamic> json) {
type = json['type']; type = json['type'];
status = json['status']; status = json['status'];
dueDate = json['due_date']; dueDate = json['due_date'];
label = json['label']; label = json['label'];
nicknameColor = json['nickname_color'] == ''
? null
: int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}");
} }
} }

View File

@ -437,8 +437,7 @@ class SearchArticleItemModel {
pubTime = json['pub_time']; pubTime = json['pub_time'];
like = json['like']; like = json['like'];
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
subTitle = subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));
rankOffset = json['rank_offset']; rankOffset = json['rank_offset'];
mid = json['mid']; mid = json['mid'];
imageUrls = json['image_urls']; imageUrls = json['image_urls'];

View File

@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget {
? '已看完' ? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}', : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0, right: 6.0,
bottom: 8.0, bottom: 6.0,
type: 'gray', type: 'gray',
), ),
// 右上角 // 右上角
@ -258,27 +258,6 @@ class HistoryItem extends StatelessWidget {
), ),
), ),
), ),
videoItem.progress != 0
? Positioned(
left: 3,
right: 3,
bottom: 0,
child: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(
StyleString.imgRadius.x),
bottomRight: Radius.circular(
StyleString.imgRadius.x),
),
child: LinearProgressIndicator(
value: videoItem.progress == -1
? 100
: videoItem.progress /
videoItem.duration,
),
),
)
: const SizedBox()
], ],
), ),
VideoContent(videoItem: videoItem, ctr: ctr) VideoContent(videoItem: videoItem, ctr: ctr)

View File

@ -6,23 +6,16 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/common.dart'; import 'package:pilipala/http/common.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/rank/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart'; import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart'; import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[];
const HomePage(), RxList navigationBars = [].obs;
const RankPage(), late List defaultNavTabs;
const DynamicsPage(), late List<int> navBarSort;
const MediaPage(),
];
RxList navigationBars = defaultNavigationBars.obs;
final StreamController<bool> bottomBarStream = final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -41,10 +34,7 @@ class MainController extends GetxController {
Utils.checkUpdata(); Utils.checkUpdata();
} }
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
int defaultHomePage =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
selectedIndex = defaultNavigationBars
.indexWhere((item) => item['id'] == defaultHomePage);
var userInfo = userInfoCache.get('userInfoCache'); var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
@ -53,6 +43,7 @@ class MainController extends GetxController {
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) { if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic(); getUnreadDynamic();
} }
setNavBarConfig();
} }
void onBackPressed(BuildContext context) { void onBackPressed(BuildContext context) {
@ -93,4 +84,21 @@ class MainController extends GetxController {
} }
navigationBars.refresh(); navigationBars.refresh();
} }
void setNavBarConfig() async {
defaultNavTabs = [...defaultNavigationBars];
navBarSort =
setting.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]);
defaultNavTabs.retainWhere((item) => navBarSort.contains(item['id']));
defaultNavTabs.sort((a, b) =>
navBarSort.indexOf(a['id']).compareTo(navBarSort.indexOf(b['id'])));
navigationBars.value = defaultNavTabs;
int defaultHomePage =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
int defaultIndex =
navigationBars.indexWhere((item) => item['id'] == defaultHomePage);
// 如果找不到匹配项默认索引设置为0或其他合适的值
selectedIndex = defaultIndex != -1 ? defaultIndex : 0;
pages = navigationBars.map<Widget>((e) => e['page']).toList();
}
} }

View File

@ -281,8 +281,8 @@ class _MemberPageState extends State<MemberPage>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data; Map data = snapshot.data!;
if (data != null && data['status']) { if (data['status']) {
return Obx( return Obx(
() => Stack( () => Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
@ -302,14 +302,7 @@ class _MemberPageState extends State<MemberPage>
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium! .titleMedium!
.copyWith( .copyWith(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
color: _memberController.memberInfo.value
.vip!.nicknameColor !=
null
? Color(_memberController.memberInfo
.value.vip!.nicknameColor!)
: null),
)), )),
const SizedBox(width: 2), const SizedBox(width: 2),
if (_memberController.memberInfo.value.sex == '') if (_memberController.memberInfo.value.sex == '')

View File

@ -18,32 +18,45 @@ class MemberSeasonsPanel extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
MemberSeasonsList item = data!.seasonsList![index]; MemberSeasonsList item = data!.seasonsList![index];
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12, right: 4),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( Padding(
onTap: () => Get.toNamed( padding: const EdgeInsets.only(bottom: 12, left: 4),
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), child: Row(
title: Text( children: [
Text(
item.meta!.name!, item.meta!.name!,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall!, style: Theme.of(context).textTheme.titleSmall!,
), ),
dense: true, const SizedBox(width: 10),
leading: PBadge( PBadge(
stack: 'relative', stack: 'relative',
size: 'small', size: 'small',
text: item.meta!.total.toString(), text: item.meta!.total.toString(),
), ),
trailing: const Icon( const Spacer(),
SizedBox(
width: 35,
height: 35,
child: IconButton(
onPressed: () => Get.toNamed(
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
icon: const Icon(
Icons.arrow_forward, Icons.arrow_forward,
size: 20, size: 20,
), ),
), ),
const SizedBox(height: 10), )
],
),
),
LayoutBuilder( LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
return GridView.builder( return GridView.builder(

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart';
import '../../../models/common/nav_bar_config.dart';
class NavigationBarSetPage extends StatefulWidget {
const NavigationBarSetPage({super.key});
@override
State<NavigationBarSetPage> createState() => _NavigationbarSetPageState();
}
class _NavigationbarSetPageState extends State<NavigationBarSetPage> {
Box settingStorage = GStrorage.setting;
late List defaultNavTabs;
late List<int> navBarSort;
@override
void initState() {
super.initState();
defaultNavTabs = defaultNavigationBars;
navBarSort = settingStorage
.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]);
// 对 tabData 进行排序
defaultNavTabs.sort((a, b) {
int indexA = navBarSort.indexOf(a['id']);
int indexB = navBarSort.indexOf(b['id']);
// 如果类型在 sortOrder 中不存在,则放在末尾
if (indexA == -1) indexA = navBarSort.length;
if (indexB == -1) indexB = navBarSort.length;
return indexA.compareTo(indexB);
});
}
void saveEdit() {
List<int> sortedTabbar = defaultNavTabs
.where((i) => navBarSort.contains(i['id']))
.map<int>((i) => i['id'])
.toList();
settingStorage.put(SettingBoxKey.navBarSort, sortedTabbar);
SmartDialog.showToast('保存成功,下次启动时生效');
}
void onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final tabsItem = defaultNavTabs.removeAt(oldIndex);
defaultNavTabs.insert(newIndex, tabsItem);
});
}
@override
Widget build(BuildContext context) {
final listTiles = [
for (int i = 0; i < defaultNavTabs.length; i++) ...[
CheckboxListTile(
key: Key(defaultNavTabs[i]['label']),
value: navBarSort.contains(defaultNavTabs[i]['id']),
onChanged: (bool? newValue) {
int tabTypeId = defaultNavTabs[i]['id'];
if (!newValue!) {
navBarSort.remove(tabTypeId);
} else {
navBarSort.add(tabTypeId);
}
setState(() {});
},
title: Text(defaultNavTabs[i]['label']),
secondary: const Icon(Icons.drag_indicator_rounded),
enabled: defaultNavTabs[i]['id'] != 3,
)
]
];
return Scaffold(
appBar: AppBar(
title: const Text('Navbar编辑'),
actions: [
TextButton(onPressed: () => saveEdit(), child: const Text('保存')),
const SizedBox(width: 12)
],
),
body: ReorderableListView(
onReorder: onReorder,
physics: const NeverScrollableScrollPhysics(),
footer: SizedBox(
height: MediaQuery.of(context).padding.bottom + 30,
),
children: listTiles,
),
);
}
}

View File

@ -284,12 +284,17 @@ class _StyleSettingState extends State<StyleSetting> {
onTap: () => Get.toNamed('/tabbarSetting'), onTap: () => Get.toNamed('/tabbarSetting'),
title: Text('首页tabbar', style: titleStyle), title: Text('首页tabbar', style: titleStyle),
), ),
ListTile(
dense: false,
onTap: () => Get.toNamed('/navbarSetting'),
title: Text('navbar设置', style: titleStyle),
),
if (Platform.isAndroid) if (Platform.isAndroid)
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/displayModeSetting'), onTap: () => Get.toNamed('/displayModeSetting'),
title: Text('屏幕帧率', style: titleStyle), title: Text('屏幕帧率', style: titleStyle),
) ),
], ],
), ),
); );

View File

@ -44,7 +44,6 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
setState(() { setState(() {
_tempValue = value as T; _tempValue = value as T;
}); });
Navigator.pop(context, _tempValue);
}, },
), ),
] ]
@ -52,6 +51,19 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
), ),
); );
}), }),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () => Navigator.pop(context, _tempValue),
child: const Text('确定'),
)
],
); );
} }
} }

View File

@ -46,40 +46,4 @@ class SubController extends GetxController {
Future onLoad() async { Future onLoad() async {
querySubFolder(type: 'onload'); querySubFolder(type: 'onload');
} }
// 取消订阅
Future<void> cancelSub(SubFolderItemData subFolderItem) async {
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: const Text('提示'),
content: const Text('确定取消订阅吗?'),
actions: [
TextButton(
onPressed: () {
Get.back();
},
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await UserHttp.cancelSub(seasonId: subFolderItem.id!);
if (res['status']) {
subFolderData.value.list!.remove(subFolderItem);
subFolderData.update((val) {});
SmartDialog.showToast('取消订阅成功');
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
child: const Text('确定'),
),
],
),
);
}
} }

View File

@ -58,8 +58,7 @@ class _SubPageState extends State<SubPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
return SubItem( return SubItem(
subFolderItem: subFolderItem:
_subController.subFolderData.value.list![index], _subController.subFolderData.value.list![index]);
cancelSub: _subController.cancelSub);
}, },
), ),
); );

View File

@ -8,12 +8,7 @@ import '../../../models/user/sub_folder.dart';
class SubItem extends StatelessWidget { class SubItem extends StatelessWidget {
final SubFolderItemData subFolderItem; final SubFolderItemData subFolderItem;
final Function(SubFolderItemData) cancelSub; const SubItem({super.key, required this.subFolderItem});
const SubItem({
super.key,
required this.subFolderItem,
required this.cancelSub,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -56,10 +51,7 @@ class SubItem extends StatelessWidget {
}, },
), ),
), ),
VideoContent( VideoContent(subFolderItem: subFolderItem)
subFolderItem: subFolderItem,
cancelSub: cancelSub,
)
], ],
), ),
); );
@ -72,8 +64,7 @@ class SubItem extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final SubFolderItemData subFolderItem; final SubFolderItemData subFolderItem;
final Function(SubFolderItemData)? cancelSub; const VideoContent({super.key, required this.subFolderItem});
const VideoContent({super.key, required this.subFolderItem, this.cancelSub});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -109,24 +100,6 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 35,
width: 35,
child: IconButton(
onPressed: () => cancelSub?.call(subFolderItem),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.outline,
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
),
icon: const Icon(Icons.delete_outline, size: 18),
),
)
],
)
], ],
), ),
), ),

View File

@ -91,7 +91,7 @@ class VideoDetailController extends GetxController
late bool enableCDN; late bool enableCDN;
late int? cacheVideoQa; late int? cacheVideoQa;
late String cacheDecode; late String cacheDecode;
late int defaultAudioQa; late int cacheAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr; PersistentBottomSheetController? replyReplyBottomSheetCtr;
RxList<SubTitileContentModel> subtitleContents = RxList<SubTitileContentModel> subtitleContents =
@ -146,7 +146,7 @@ class VideoDetailController extends GetxController
// 预设的解码格式 // 预设的解码格式
cacheDecode = setting.get(SettingBoxKey.defaultDecode, cacheDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code); defaultValue: VideoDecodeFormats.values.last.code);
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code); defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!); oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
getSubtitle(); getSubtitle();
@ -353,9 +353,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) { if (audiosList.isNotEmpty) {
final List<int> numbers = audiosList.map((map) => map.id!).toList(); final List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers); int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
if (!numbers.contains(defaultAudioQa) && if (!numbers.contains(cacheAudioQa) &&
numbers.any((e) => e > defaultAudioQa)) { numbers.any((e) => e > cacheAudioQa)) {
closestNumber = 30280; closestNumber = 30280;
} }
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);

View File

@ -1,4 +1,3 @@
import 'package:expandable/expandable.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -17,6 +16,7 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../../../http/user.dart'; import '../../../../http/user.dart';
import '../widgets/expandable_section.dart';
import 'widgets/action_item.dart'; import 'widgets/action_item.dart';
import 'widgets/fav_panel.dart'; import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
@ -140,8 +140,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
late bool enableAi; late bool enableAi;
bool isProcessing = false; bool isProcessing = false;
RxBool isExpand = false.obs; RxBool isExpand = false.obs;
late ExpandableController _expandableCtr;
void Function()? handleState(Future Function() action) { void Function()? handleState(Future Function() action) {
return isProcessing return isProcessing
? null ? null
@ -165,7 +163,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
follower = Utils.numFormat(videoIntroController.userStat['follower']); follower = Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus; followStatus = videoIntroController.followStatus;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
} }
// 收藏 // 收藏
@ -219,7 +216,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
showIntroDetail() { showIntroDetail() {
feedBack(); feedBack();
isExpand.value = !(isExpand.value); isExpand.value = !(isExpand.value);
_expandableCtr.toggle();
} }
// 用户主页 // 用户主页
@ -243,12 +239,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
); );
} }
@override
void dispose() {
_expandableCtr.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData t = Theme.of(context); final ThemeData t = Theme.of(context);
@ -266,34 +256,14 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
child: ExpandablePanel( child: Text(
controller: _expandableCtr,
collapsed: Text(
widget.videoDetail!.title!, widget.videoDetail!.title!,
softWrap: true, style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, 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,
),
), ),
), ),
Stack( Stack(
@ -358,16 +328,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
/// 视频简介 /// 视频简介
ExpandablePanel( Obx(
controller: _expandableCtr, () => ExpandedSection(
collapsed: const SizedBox(height: 0), expand: isExpand.value,
expanded: IntroDetail(videoDetail: widget.videoDetail!), begin: 0,
theme: const ExpandableThemeData( end: 1,
animationDuration: Duration(milliseconds: 300), child: IntroDetail(videoDetail: widget.videoDetail!),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
), ),
), ),

View File

@ -149,17 +149,14 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
delegate: _MySliverPersistentHeaderDelegate( delegate: _MySliverPersistentHeaderDelegate(
child: Container( child: Container(
height: 40, height: 40,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), padding: const EdgeInsets.fromLTRB(12, 6, 6, 0),
color: Theme.of(context).colorScheme.surface,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Obx( Text(
() => Text(
'${_videoReplyController.sortTypeLabel.value}评论', '${_videoReplyController.sortTypeLabel.value}评论',
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
), ),
),
SizedBox( SizedBox(
height: 35, height: 35,
child: TextButton.icon( child: TextButton.icon(

View File

@ -525,18 +525,14 @@ InlineSpan buildContent(
if (jumpUrlKeysList.isNotEmpty) { if (jumpUrlKeysList.isNotEmpty) {
patternStr += '|${jumpUrlKeysList.join('|')}'; patternStr += '|${jumpUrlKeysList.join('|')}';
} }
RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}');
final RegExp pattern = RegExp(patternStr); final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = []; List<String> matchedStrs = [];
void addPlainTextSpan(str) { void addPlainTextSpan(str) {
spanChilds.add( spanChilds.add(TextSpan(
TextSpan(
text: str, text: str,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => ..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem), replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem)));
),
);
} }
// 分割文本并处理每个部分 // 分割文本并处理每个部分
@ -738,36 +734,8 @@ InlineSpan buildContent(
return ''; return '';
}, },
onNonMatch: (String nonMatchStr) { onNonMatch: (String nonMatchStr) {
return nonMatchStr.splitMapJoin( addPlainTextSpan(nonMatchStr);
bv23Regex, return nonMatchStr;
onMatch: (Match match) {
String matchStr = match[0]!;
spanChilds.add(
TextSpan(
text: ' $matchStr ',
style: isVideoPage
? TextStyle(
color: Theme.of(context).colorScheme.primary,
)
: null,
recognizer: TapGestureRecognizer()
..onTap = () => Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': matchStr
},
),
),
);
return '';
},
onNonMatch: (String nonMatchOtherStr) {
addPlainTextSpan(nonMatchOtherStr);
return nonMatchOtherStr;
},
);
}, },
); );

View File

@ -142,10 +142,9 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _keyboardHeight = EdgeInsets.fromViewPadding( double keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio) View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom; .bottom;
print('_keyboardHeight: $_keyboardHeight');
return Container( return Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -236,11 +235,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: toolbarType == 'input' height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
? (_keyboardHeight > keyboardHeight
? _keyboardHeight
: keyboardHeight)
: emoteHeight,
child: EmotePanel( child: EmotePanel(
onChoose: (package, emote) => onChooseEmote(package, emote), onChoose: (package, emote) => onChooseEmote(package, emote),
), ),

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/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/services/shutdown_timer_service.dart'; import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../http/danmaku.dart';
import '../../../../models/common/search_type.dart'; import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart'; import '../../../../models/video_detail_res.dart';
import '../introduction/index.dart'; import '../introduction/index.dart';
@ -53,7 +52,7 @@ class _HeaderControlState extends State<HeaderControl> {
final Box<dynamic> videoStorage = GStrorage.video; final Box<dynamic> videoStorage = GStrorage.video;
late List<double> speedsList; late List<double> speedsList;
double buttonSpace = 8; double buttonSpace = 8;
RxBool isFullScreen = false.obs; bool showTitle = false;
late String heroTag; late String heroTag;
late VideoIntroController videoIntroController; late VideoIntroController videoIntroController;
late VideoDetailData videoDetail; late VideoDetailData videoDetail;
@ -70,8 +69,13 @@ class _HeaderControlState extends State<HeaderControl> {
} }
void fullScreenStatusListener() { void fullScreenStatusListener() {
widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) { widget.videoDetailCtr!.plPlayerController.isFullScreen
isFullScreen.value = val; .listen((bool isFullScreen) {
if (isFullScreen) {
showTitle = true;
} else {
showTitle = false;
}
/// TODO setState() called after dispose() /// TODO setState() called after dispose()
if (mounted) { 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 { void scheduleExit() async {
const List<int> scheduleTimeChoices = [ const List<int> scheduleTimeChoices = [
@ -1156,7 +1079,7 @@ class _HeaderControlState extends State<HeaderControl> {
}, },
), ),
SizedBox(width: buttonSpace), SizedBox(width: buttonSpace),
if (isFullScreen.value && if (showTitle &&
isLandscape && isLandscape &&
widget.videoType == SearchType.video) ...[ widget.videoType == SearchType.video) ...[
Column( Column(
@ -1208,43 +1131,6 @@ class _HeaderControlState extends State<HeaderControl> {
// ), // ),
// fuc: () => _.screenshot(), // 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), SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...<Widget>[ if (Platform.isAndroid) ...<Widget>[
SizedBox( SizedBox(

View File

@ -39,6 +39,7 @@ import '../pages/setting/pages/color_select.dart';
import '../pages/setting/pages/display_mode.dart'; import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/font_size_select.dart';
import '../pages/setting/pages/home_tabbar_set.dart'; import '../pages/setting/pages/home_tabbar_set.dart';
import '../pages/setting/pages/navigation_bar_set.dart';
import '../pages/setting/pages/play_gesture_set.dart'; import '../pages/setting/pages/play_gesture_set.dart';
import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/pages/play_speed_set.dart';
import '../pages/setting/recommend_setting.dart'; import '../pages/setting/recommend_setting.dart';
@ -170,6 +171,9 @@ class Routes {
// 播放器手势 // 播放器手势
CustomGetPage( CustomGetPage(
name: '/playerGestureSet', page: () => const PlayGesturePage()), name: '/playerGestureSet', page: () => const PlayGesturePage()),
// navigation bar
CustomGetPage(
name: '/navbarSetting', page: () => const NavigationBarSetPage()),
]; ];
} }

View File

@ -19,17 +19,7 @@ class Em {
return regCate(matchStr); return regCate(matchStr);
}, onNonMatch: (String str) { }, onNonMatch: (String str) {
if (str != '') { if (str != '') {
str = decodeHtmlEntities(str); str = str
Map map = {'type': 'text', 'text': str};
res.add(map);
}
return str;
});
return res;
}
static String decodeHtmlEntities(String title) {
return title
.replaceAll('&lt;', '<') .replaceAll('&lt;', '<')
.replaceAll('&gt;', '>') .replaceAll('&gt;', '>')
.replaceAll('&#34;', '"') .replaceAll('&#34;', '"')
@ -37,7 +27,12 @@ class Em {
.replaceAll('&quot;', '"') .replaceAll('&quot;', '"')
.replaceAll('&apos;', "'") .replaceAll('&apos;', "'")
.replaceAll('&nbsp;', " ") .replaceAll('&nbsp;', " ")
.replaceAll('&amp;', "&") .replaceAll('&amp;', "&");
.replaceAll('&#x27;', "'"); Map map = {'type': 'text', 'text': str};
res.add(map);
}
return str;
});
return res;
} }
} }

View File

@ -148,7 +148,8 @@ class SettingBoxKey {
hideTabBar = 'hideTabBar', // 收起底栏 hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort', // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode', dynamicBadgeMode = 'dynamicBadgeMode',
enableGradientBg = 'enableGradientBg'; enableGradientBg = 'enableGradientBg',
navBarSort = 'navBarSort';
} }
class LocalCacheKey { class LocalCacheKey {

View File

@ -51,7 +51,7 @@ class Utils {
} }
if (time < 3600) { if (time < 3600) {
if (time == 0) { if (time == 0) {
return '00:00'; return time;
} }
final int minute = time ~/ 60; final int minute = time ~/ 60;
final double res = time / 60; final double res = time / 60;
@ -208,24 +208,7 @@ class Utils {
static int findClosestNumber(int target, List<int> numbers) { static int findClosestNumber(int target, List<int> numbers) {
int minDiff = 127; int minDiff = 127;
int closestNumber = 0; // 初始化为0表示没有找到比目标值小的整数 late int closestNumber;
// 向下查找
try {
for (int number in numbers) {
if (number < target) {
int diff = target - number; // 计算目标值与当前整数的差值
if (diff < minDiff) {
minDiff = diff;
closestNumber = number;
}
}
}
} catch (_) {}
// 向上查找
if (closestNumber == 0) {
try { try {
for (int number in numbers) { for (int number in numbers) {
int diff = (number - target).abs(); int diff = (number - target).abs();
@ -236,7 +219,6 @@ class Utils {
} }
} }
} catch (_) {} } catch (_) {}
}
return closestNumber; return closestNumber;
} }

View File

@ -433,14 +433,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.3" 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: extended_image:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.22+1022 version: 1.0.21+1021
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@ -142,8 +142,6 @@ dependencies:
path: 1.8.3 path: 1.8.3
# 电池优化 # 电池优化
disable_battery_optimization: ^1.1.1 disable_battery_optimization: ^1.1.1
# 展开/收起
expandable: ^5.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: