Compare commits

...

54 Commits

Author SHA1 Message Date
83b0ff02e4 fix: 图片预览放大、取消下滑关闭图片预览 2024-03-03 18:32:13 +08:00
8109314aaf opt: url scheme优化 issues #581 2024-03-03 15:20:59 +08:00
c4b3446956 Merge branch 'fix-replyPanelScroll' 2024-03-03 12:54:26 +08:00
d804d95d78 Merge branch 'fix-pip' 2024-03-03 12:53:48 +08:00
234dfe9d64 Merge branch 'fix' 2024-03-03 12:53:40 +08:00
c20df8fd81 fix: enable pip 2024-03-03 12:22:10 +08:00
19f0b1b28f fix: 动态专栏重复 2024-03-03 11:53:15 +08:00
481fa0d934 feat: 默认启动页设置 issues #483 2024-03-03 11:09:52 +08:00
caca16a957 fix: 动态页面upPanel不刷新 2024-03-03 09:57:41 +08:00
602d795909 Merge branch 'main' into fix 2024-03-03 09:37:28 +08:00
800f714f4a mod: 视频详情页appBar 2024-03-03 00:52:47 +08:00
75f569cb79 mod: 合集布局 2024-03-03 00:15:16 +08:00
0e888537e8 mod: yml rename 2024-03-02 22:37:56 +08:00
a3ce15bd9e mod: CI format 2024-03-02 22:27:02 +08:00
40f94e7ace Merge pull request #587 from VillagerTom/sending-beta-to-tg-channel
推送至main分支时编译为beta版本,发送到Telegram频道
2024-03-02 22:24:19 +08:00
370dcaf419 mod: 用户登录状态msg取值 2024-03-02 16:05:15 +08:00
f81f348a3e fix: 视频详情页评论下拉刷新 issues #486 2024-03-02 15:51:24 +08:00
4191cafe78 fix: 推荐卡片单列布局 2024-03-02 14:47:07 +08:00
ae33cbf7ca fix: 搜索框默认搜索词溢出 2024-03-02 13:02:43 +08:00
fca7c36203 mod: 动态页面upPanel 2024-03-02 12:56:16 +08:00
5fc783ebc2 Merge branch 'design' 2024-03-02 11:46:20 +08:00
98122aeaae fix: audioHandler null 2024-03-02 11:45:36 +08:00
f0d8e2a122 feat: 播放器控制栏动画开关 2024-03-02 11:19:18 +08:00
f815affff9 opt: 播放器控制栏动画 2024-03-02 00:40:53 +08:00
fce701090a Merge branch 'main' into design 2024-03-02 00:39:01 +08:00
d6da2a8a47 fix: headerControl bvid丢失 2024-03-01 23:55:19 +08:00
b3e162c8d3 Merge branch 'main' into fix 2024-03-01 23:45:12 +08:00
e5eae93a78 fix: 私信页面表情面板 issues #588 2024-03-01 23:01:10 +08:00
962dcca6d4 Merge branch 'fix' 2024-03-01 00:15:14 +08:00
be56fb721f fix: 私信页面表情面板 issues #588 2024-03-01 00:14:42 +08:00
ce1c80fd86 fix: 动态最新关注横行拉伸 issuse #580 2024-02-29 23:42:09 +08:00
33ef18ef1d fix: 评论jumpUrl正则转义 2024-02-29 00:30:55 +08:00
ba61e38c9b Merge branch 'fix-dynamicReplySeekTime' 2024-02-29 00:00:09 +08:00
0b0db1a2b1 mod: videoPage path判断 2024-02-28 23:59:47 +08:00
a9d73a9f1b fix: 动态标题未显示 2024-02-28 23:51:30 +08:00
0b5397ec00 fix: 动态评论区seek error 2024-02-28 23:29:02 +08:00
466214b26a fix: statusBarIcon color 2024-02-28 23:17:03 +08:00
699be4125b 将版本号中的alpha改为beta; 加回之前删去的“v” 2024-02-28 16:22:14 +08:00
45cc46d6d6 重命名:.github/workflows/alpha.yml -> .github/workflows/CI.yml 2024-02-28 15:38:17 +08:00
3f9fcabc2d Revert "将alpha.yml的workflow name改为alpha, 避免混淆"
This reverts commit 04186cdd5b.
2024-02-28 15:36:30 +08:00
65d2bfd844 升级至channel-post@v1.0.7, 支持传输大文件 2024-02-28 15:35:28 +08:00
4642c2a847 将git log pretty format中raw body替换为subject, 避免revert commit多行输出 2024-02-28 15:35:28 +08:00
04186cdd5b 将alpha.yml的workflow name改为alpha, 避免混淆 2024-02-28 15:35:28 +08:00
40cc4e0dd1 channel-post@v1.0.5重复发送文件,改为v1.0.4 2024-02-28 15:35:28 +08:00
95bc4a9f46 在Telegram消息中显示最后一次提交信息 2024-02-28 15:35:28 +08:00
3bf3fd9a46 使用参数fetch-depth: 0取得所有分支和tags, 末端提交改回HEAD 2024-02-28 15:35:28 +08:00
83ad11402f 😅注释符被识别为文件名的一部分 2024-02-28 15:35:28 +08:00
cfeb0588c1 取消发送其他架构APK, 减少发送文件大小 2024-02-28 15:35:28 +08:00
381e832f3c 修正架构名称拼写错误 2024-02-28 15:35:28 +08:00
6c20a434ed 列出文件 2024-02-28 15:35:28 +08:00
fc2da3ce57 使checkout action克隆指定分支; 统一代码缩进 2024-02-28 15:35:28 +08:00
a3abed0a03 新增alpha.yml, 用于编译推送至alpha分支的代码并发送至Telegram频道 2024-02-28 15:35:28 +08:00
db03cdd442 fix: List dataType 2024-02-28 00:45:13 +08:00
542975d0ec feat: 全屏手势设置 issues #517 2024-02-28 00:34:46 +08:00
53 changed files with 1095 additions and 527 deletions

208
.github/workflows/beta_ci.yml vendored Normal file
View File

@ -0,0 +1,208 @@
name: Pilipala Beta
on:
workflow_dispatch:
push:
branches:
- "main"
paths-ignore:
- "**.md"
- "**.txt"
- ".github/**"
- ".idea/**"
- "!.github/workflows/**"
jobs:
update_version:
name: Read and update version
runs-on: ubuntu-latest
outputs:
# 定义输出变量 version以便在其他job中引用
new_version: ${{ steps.version.outputs.new_version }}
last_commit: ${{ steps.get-last-commit.outputs.last_commit }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0
- name: 获取first parent commit次数
id: get-first-parent-commit-count
run: |
version=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
recent_release_tag=$(git tag -l | grep $version | egrep -v "[-|+]" || true)
if [[ "x$recent_release_tag" == "x" ]]; then
echo "当前版本tag不存在请手动生成tag."
exit 1
fi
git log --oneline --first-parent $recent_release_tag..HEAD
first_parent_commit_count=$(git rev-list --first-parent --count $recent_release_tag..HEAD)
echo "count=$first_parent_commit_count" >> $GITHUB_OUTPUT
- name: 获取最后一次提交
id: get-last-commit
run: |
last_commit=$(git log -1 --pretty="%h %s" --first-parent)
echo "last_commit=$last_commit" >> $GITHUB_OUTPUT
- name: 更新版本号
id: version
run: |
# 读取版本号
VERSION=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
# 获取GitHub Actions的run_number
#RUN_NUMBER=${{ github.run_number }}
# 构建新版本号
NEW_VERSION=$VERSION-beta.${{ steps.get-first-parent-commit-count.outputs.count }}
# 输出新版本号
echo "New version: $NEW_VERSION"
# 设置新版本号为输出变量
echo "new_version=$NEW_VERSION" >>$GITHUB_OUTPUT
android:
name: Build CI (Android)
needs: update_version
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
- name: 构建Java环境
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "17"
token: ${{secrets.GIT_TOKEN}}
- name: 检查缓存
uses: actions/cache@v2
id: cache-flutter
with:
path: /root/flutter-sdk
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
- name: 安装Flutter
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: 3.16.5
channel: any
- name: 下载项目依赖
run: flutter pub get
- name: 解码生成 jks
run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
- name: 更新版本号
id: version
run: |
# 更新pubspec.yaml文件中的版本号
sed -i "s/version: .*+/version: ${{ needs.update_version.outputs.new_version }}+/g" pubspec.yaml
- name: flutter build apk
run: flutter build apk --release --split-per-abi
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
- name: 重命名应用
run: |
for file in build/app/outputs/flutter-apk/app-*.apk; do
if [[ $file =~ app-(.?*)release.apk ]]; then
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}v${{ needs.update_version.outputs.new_version }}.apk"
mv "$file" "$new_file_name"
fi
done
- name: 上传
uses: actions/upload-artifact@v3
with:
name: Pilipala-Beta
path: |
build/app/outputs/flutter-apk/Pili-*.apk
iOS:
name: Build CI (iOS)
needs: update_version
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
- name: 安装Flutter
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2.10.0
with:
cache: true
flutter-version: 3.16.5
- name: 更新版本号
id: version
run: |
# 更新pubspec.yaml文件中的版本号
sed -i "" "s/version: .*+/version: ${{ needs.update_version.outputs.new_version }}+/g" pubspec.yaml
- name: flutter build ipa
run: |
flutter build ios --release --no-codesign
ln -sf ./build/ios/iphoneos Payload
zip -r9 app.ipa Payload/runner.app
- name: 重命名应用
run: |
DATE=${{ steps.date.outputs.date }}
for file in app.ipa; do
new_file_name="build/Pili-v${{ needs.update_version.outputs.new_version }}.ipa"
mv "$file" "$new_file_name"
done
- name: 上传
uses: actions/upload-artifact@v3
with:
if-no-files-found: error
name: Pilipala-Beta
path: |
build/Pili-*.ipa
upload:
runs-on: ubuntu-latest
needs:
- update_version
- android
- iOS
steps:
- uses: actions/download-artifact@v3
with:
name: Pilipala-Beta
path: ./Pilipala-Beta
- name: 发送到Telegram频道
uses: xireiki/channel-post@v1.0.7
with:
bot_token: ${{ secrets.BOT_TOKEN }}
chat_id: ${{ secrets.CHAT_ID }}
large_file: true
api_id: ${{ secrets.TELEGRAM_API_ID }}
api_hash: ${{ secrets.TELEGRAM_API_HASH }}
method: sendFile
path: Pilipala-Beta/*
parse_mode: Markdown
context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}](${{ github.event.head_commit.url }})"

View File

@ -223,6 +223,10 @@
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="b23.tv"
android:pathPattern="/*" />
<data android:scheme="https" android:host="space.bilibili.com"
android:pathPattern="/*" />
</intent-filter>
</activity>

View File

@ -231,6 +231,7 @@ class VideoContent extends StatelessWidget {
const SizedBox(height: 2),
VideoStat(
videoItem: videoItem,
crossAxisCount: crossAxisCount,
),
],
if (crossAxisCount == 1) const SizedBox(height: 4),
@ -294,6 +295,7 @@ class VideoContent extends StatelessWidget {
),
VideoStat(
videoItem: videoItem,
crossAxisCount: crossAxisCount,
),
const Spacer(),
],
@ -317,10 +319,12 @@ class VideoContent extends StatelessWidget {
class VideoStat extends StatelessWidget {
final dynamic videoItem;
final int crossAxisCount;
const VideoStat({
Key? key,
required this.videoItem,
required this.crossAxisCount,
}) : super(key: key);
@override
@ -337,7 +341,7 @@ class VideoStat extends StatelessWidget {
danmu: videoItem.stat.danmu,
),
if (videoItem is RecVideoItemModel) ...<Widget>[
const Spacer(),
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
RichText(
maxLines: 1,
text: TextSpan(

View File

@ -0,0 +1,12 @@
enum FullScreenGestureMode {
/// 从上滑到下
fromToptoBottom,
/// 从下滑到上
fromBottomtoTop,
}
extension FullScreenGestureModeExtension on FullScreenGestureMode {
String get values => ['fromToptoBottom', 'fromBottomtoTop'][index];
String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏'][index];
}

View File

@ -0,0 +1,4 @@
library commonn_model;
export './business_type.dart';
export './gesture_mode.dart';

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
const defaultNavigationBars = [
{
'id': 0,
'icon': Icon(
Icons.home_outlined,
size: 21,
),
'selectIcon': Icon(
Icons.home,
size: 21,
),
'label': "首页",
'count': 0,
},
{
'id': 1,
'icon': Icon(
Icons.motion_photos_on_outlined,
size: 21,
),
'selectIcon': Icon(
Icons.motion_photos_on,
size: 21,
),
'label': "动态",
'count': 0,
},
{
'id': 2,
'icon': Icon(
Icons.video_collection_outlined,
size: 20,
),
'selectIcon': Icon(
Icons.video_collection,
size: 21,
),
'label': "媒体库",
'count': 0,
}
];

View File

@ -2,18 +2,28 @@ class FollowUpModel {
FollowUpModel({
this.liveUsers,
this.upList,
this.liveList,
this.myInfo,
});
LiveUsers? liveUsers;
List<UpItem>? upList;
List<LiveUserItem>? liveList;
MyInfo? myInfo;
FollowUpModel.fromJson(Map<String, dynamic> json) {
liveUsers = json['live_users'] != null
? LiveUsers.fromJson(json['live_users'])
: null;
liveList = json['live_users'] != null
? json['live_users']['items']
.map<LiveUserItem>((e) => LiveUserItem.fromJson(e))
.toList()
: [];
upList = json['up_list'] != null
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()
: [];
myInfo = json['my_info'] != null ? MyInfo.fromJson(json['my_info']) : null;
}
}
@ -93,3 +103,21 @@ class UpItem {
uname = json['uname'];
}
}
class MyInfo {
MyInfo({
this.face,
this.mid,
this.name,
});
String? face;
int? mid;
String? name;
MyInfo.fromJson(Map<String, dynamic> json) {
face = json['face'];
mid = json['mid'];
name = json['name'];
}
}

View File

@ -15,7 +15,7 @@ class FavFolderData {
? json['list']
.map<FavFolderItemData>((e) => FavFolderItemData.fromJson(e))
.toList()
: [FavFolderItemData()];
: <FavFolderItemData>[];
hasMore = json['has_more'];
}
}

View File

@ -7,8 +7,8 @@ import 'package:pilipala/utils/storage.dart';
class BangumiController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<BangumiListItemModel> bangumiList = [BangumiListItemModel()].obs;
RxList<BangumiListItemModel> bangumiFollowList = [BangumiListItemModel()].obs;
RxList<BangumiListItemModel> bangumiList = <BangumiListItemModel>[].obs;
RxList<BangumiListItemModel> bangumiFollowList = <BangumiListItemModel>[].obs;
int _currentPage = 1;
bool isLoadingMore = true;
Box userInfoCache = GStrorage.userInfo;

View File

@ -65,6 +65,45 @@ class _BangumiPanelState extends State<BangumiPanel> {
super.dispose();
}
Widget buildPageListItem(
EpisodeItem page,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () {
Get.back();
setState(() {
changeFucCall(page, index);
});
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
'${index + 1}${page.longTitle!}',
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: page.badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
}
void showBangumiPanel() {
showBottomSheet(
context: context,
@ -106,37 +145,21 @@ class _BangumiPanelState extends State<BangumiPanel> {
child: Material(
child: ScrollablePositionedList.builder(
itemCount: widget.pages.length,
itemBuilder: (BuildContext context, int index) =>
ListTile(
onTap: () {
setState(() {
changeFucCall(widget.pages[index], index);
});
},
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == widget.pages.length - 1;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom +
20,
)
: null,
title: Text(
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
),
: buildPageListItem(
widget.pages[index],
index,
isCurrentIndex,
);
},
itemScrollController: itemScrollController,
),
),

View File

@ -139,7 +139,7 @@ class BlackListController extends GetxController {
int currentPage = 1;
int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs;
RxList<BlackListItem> blackList = <BlackListItem>[].obs;
Future queryBlacklist({type = 'init'}) async {
if (type == 'init') {

View File

@ -20,7 +20,7 @@ import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
String? offset = '';
RxList<DynamicItemModel> dynamicsList = [DynamicItemModel()].obs;
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
Rx<DynamicsType> dynamicsType = DynamicsType.values[0].obs;
RxString dynamicsTypeLabel = '全部'.obs;
final ScrollController scrollController = ScrollController();
@ -105,7 +105,7 @@ class DynamicsController extends GetxController {
onSelectType(value) async {
dynamicsType.value = filterTypeList[value]['value'];
dynamicsList.value = [DynamicItemModel()];
dynamicsList.value = <DynamicItemModel>[];
page = 1;
initialValue.value = value;
await queryFollowDynamic();
@ -249,8 +249,8 @@ class DynamicsController extends GetxController {
return {'status': false, 'msg': '账号未登录'};
}
if (type == 'init') {
upData.value.upList = [];
upData.value.liveUsers = LiveUsers();
upData.value.upList = <UpItem>[];
upData.value.liveList = <LiveUserItem>[];
}
var res = await DynamicsHttp.followUp();
if (res['status']) {
@ -258,20 +258,23 @@ class DynamicsController extends GetxController {
if (upData.value.upList!.isEmpty) {
mid.value = -1;
}
upData.value.upList!.insertAll(0, [
UpItem(face: '', uname: '全部动态', mid: -1),
UpItem(face: userInfo.face, uname: '', mid: userInfo.mid),
]);
}
return res;
}
onSelectUp(mid) async {
dynamicsType.value = DynamicsType.values[0];
dynamicsList.value = [DynamicItemModel()];
dynamicsList.value = <DynamicItemModel>[];
page = 1;
queryFollowDynamic();
}
onRefresh() async {
page = 1;
print('onRefresh');
await queryFollowUp();
await queryFollowDynamic();
}
@ -293,7 +296,7 @@ class DynamicsController extends GetxController {
dynamicsType.value = DynamicsType.values[0];
initialValue.value = 0;
SmartDialog.showToast('还原默认加载');
dynamicsList.value = [DynamicItemModel()];
dynamicsList.value = <DynamicItemModel>[];
queryFollowDynamic();
}
}

View File

@ -17,7 +17,7 @@ class DynamicDetailController extends GetxController {
int currentPage = 0;
bool isLoadingMore = false;
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
RxInt acount = 0.obs;
final ScrollController scrollController = ScrollController();

View File

@ -34,25 +34,25 @@ Widget articlePanel(item, context, {floor = 1}) {
),
const SizedBox(height: 8),
],
Text(
item.modules.moduleDynamic.major.opus.title,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 2),
if (item.modules.moduleDynamic.major.opus.summary.text !=
'undefined') ...[
Text(
item.modules.moduleDynamic.major.opus.summary.richTextNodes.first
.text,
maxLines: 4,
style: const TextStyle(height: 1.55),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
],
// Text(
// item.modules.moduleDynamic.major.opus.title,
// style: Theme.of(context)
// .textTheme
// .titleMedium!
// .copyWith(fontWeight: FontWeight.bold),
// ),
// const SizedBox(height: 2),
// if (item.modules.moduleDynamic.major.opus.summary.text !=
// 'undefined') ...[
// Text(
// item.modules.moduleDynamic.major.opus.summary.richTextNodes.first
// .text,
// maxLines: 4,
// style: const TextStyle(height: 1.55),
// overflow: TextOverflow.ellipsis,
// ),
// const SizedBox(height: 2),
// ],
picWidget(item, context)
],
),

View File

@ -45,7 +45,9 @@ class _ContentState extends State<Content> {
if (len == 1) {
OpusPicsModel pictureItem = pics.first;
picList.add(pictureItem.url!);
spanChilds.add(const TextSpan(text: '\n'));
/// 图片上方的空白间隔
// spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(

View File

@ -19,6 +19,17 @@ InlineSpan richNode(item, context) {
// 动态页面 richTextNodes 层级可能与主页动态层级不同
richTextNodes =
item.modules.moduleDynamic.major.opus.summary.richTextNodes;
if (item.modules.moduleDynamic.major.opus.title != null) {
spanChilds.add(
TextSpan(
text: item.modules.moduleDynamic.major.opus.title + '\n',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
);
}
}
if (richTextNodes == null || richTextNodes.isEmpty) {
return spacer;

View File

@ -1,16 +1,14 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/dynamics/controller.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class UpPanel extends StatefulWidget {
final FollowUpModel? upData;
final FollowUpModel upData;
const UpPanel(this.upData, {Key? key}) : super(key: key);
@override
@ -24,38 +22,22 @@ class _UpPanelState extends State<UpPanel> {
List<UpItem> upList = [];
List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
Box userInfoCache = GStrorage.userInfo;
var userInfo;
late MyInfo userInfo;
@override
void initState() {
super.initState();
upList = widget.upData!.upList!;
if (widget.upData!.liveUsers != null) {
liveList = widget.upData!.liveUsers!.items!;
}
upList.insert(
0,
UpItem(face: '', uname: '全部动态', mid: -1),
);
userInfo = userInfoCache.get('userInfoCache');
upList.insert(
1,
UpItem(
face: userInfo.face,
uname: '',
mid: userInfo.mid,
),
);
void listFormat() {
userInfo = widget.upData.myInfo!;
upList = widget.upData.upList!;
liveList = widget.upData.liveList!;
}
@override
Widget build(BuildContext context) {
listFormat();
return SliverPersistentHeader(
floating: true,
pinned: false,
delegate: _SliverHeaderDelegate(
height: 126,
height: liveList.isNotEmpty || upList.isNotEmpty ? 126 : 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
@ -90,7 +72,7 @@ class _UpPanelState extends State<UpPanel> {
color: Theme.of(context).colorScheme.background,
child: Row(
children: [
Expanded(
Flexible(
child: ListView(
scrollDirection: Axis.horizontal,
controller: scrollController,

View File

@ -10,7 +10,7 @@ class FansController extends GetxController {
int pn = 1;
int ps = 20;
int total = 0;
RxList<FansItemModel> fansList = [FansItemModel()].obs;
RxList<FansItemModel> fansList = <FansItemModel>[].obs;
late int mid;
late String name;
var userInfo;

View File

@ -58,6 +58,9 @@ class _HomePageState extends State<HomePage>
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
appBar: _homeController.enableGradientBg
? null
: AppBar(toolbarHeight: 0, elevation: 0),
body: Stack(
children: [
// gradient background
@ -412,13 +415,16 @@ class SearchBar extends StatelessWidget {
),
const SizedBox(width: 10),
Obx(
() => Text(
ctr!.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.outline),
() => Expanded(
child: Text(
ctr!.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.outline),
),
),
),
const SizedBox(width: 15),
],
),
),

View File

@ -7,7 +7,7 @@ class HotController extends GetxController {
final ScrollController scrollController = ScrollController();
final int _count = 20;
int _currentPage = 1;
RxList<HotVideoItemModel> videoList = [HotVideoItemModel()].obs;
RxList<HotVideoItemModel> videoList = <HotVideoItemModel>[].obs;
bool isLoadingMore = false;
bool flag = false;
OverlayEntry? popupDialog;

View File

@ -12,6 +12,7 @@ import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
@ -19,44 +20,7 @@ class MainController extends GetxController {
const DynamicsPage(),
const MediaPage(),
];
RxList navigationBars = [
{
'icon': const Icon(
Icons.home_outlined,
size: 21,
),
'selectIcon': const Icon(
Icons.home,
size: 21,
),
'label': "首页",
'count': 0,
},
{
'icon': const Icon(
Icons.motion_photos_on_outlined,
size: 21,
),
'selectIcon': const Icon(
Icons.motion_photos_on,
size: 21,
),
'label': "动态",
'count': 0,
},
{
'icon': const Icon(
Icons.video_collection_outlined,
size: 20,
),
'selectIcon': const Icon(
Icons.video_collection,
size: 21,
),
'label': "媒体库",
'count': 0,
}
].obs;
RxList navigationBars = defaultNavigationBars.obs;
final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast();
Box setting = GStrorage.setting;
@ -75,6 +39,10 @@ class MainController extends GetxController {
Utils.checkUpdata();
}
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');
userLogin.value = userInfo != null;
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(

View File

@ -20,7 +20,7 @@ class MemberController extends GetxController {
Box userInfoCache = GStrorage.userInfo;
late int ownerMid;
// 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
RxList<VListItemModel>? archiveList = <VListItemModel>[].obs;
dynamic userInfo;
RxInt attribute = (-1).obs;
RxString attributeText = '关注'.obs;

View File

@ -135,115 +135,103 @@ class _ImagePreviewState extends State<ImagePreview>
),
body: Stack(
children: [
DismissiblePage(
backgroundColor: Colors.transparent,
onDismissed: () {
Navigator.of(context).pop();
},
// Note that scrollable widget inside DismissiblePage might limit the functionality
// If scroll direction matches DismissiblePage direction
direction: DismissiblePageDismissDirection.down,
disabled: _dismissDisabled,
isFullScreen: true,
child: GestureDetector(
onLongPress: () => onOpenMenu(),
child: ExtendedImageGesturePageView.builder(
controller: ExtendedPageController(
initialPage: _previewController.initialPage.value,
pageSpacing: 0,
),
onPageChanged: (int index) =>
_previewController.onChange(index),
canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0,
itemCount: widget.imgList!.length,
itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network(
widget.imgList![index],
fit: BoxFit.contain,
mode: ExtendedImageMode.gesture,
onDoubleTap: (ExtendedImageGestureState state) {
final Offset? pointerDownPosition =
state.pointerDownPosition;
final double? begin = state.gestureDetails!.totalScale;
double end;
//remove old
_doubleClickAnimation
?.removeListener(_doubleClickAnimationListener);
//stop pre
_doubleClickAnimationController.stop();
//reset to use
_doubleClickAnimationController.reset();
if (begin == doubleTapScales[0]) {
setState(() {
_dismissDisabled = true;
});
end = doubleTapScales[1];
} else {
setState(() {
_dismissDisabled = false;
});
end = doubleTapScales[0];
}
_doubleClickAnimationListener = () {
state.handleDoubleTap(
scale: _doubleClickAnimation!.value,
doubleTapPosition: pointerDownPosition);
};
_doubleClickAnimation = _doubleClickAnimationController
.drive(Tween<double>(begin: begin, end: end));
_doubleClickAnimation!
.addListener(_doubleClickAnimationListener);
_doubleClickAnimationController.forward();
},
// ignore: body_might_complete_normally_nullable
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =
state.loadingProgress;
final double? progress =
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 150.0,
child: LinearProgressIndicator(
value: progress,
color: Colors.white,
),
),
// const SizedBox(height: 10.0),
// Text('${((progress ?? 0.0) * 100).toInt()}%',),
],
),
);
}
},
initGestureConfigHandler: (ExtendedImageState state) {
return GestureConfig(
inPageView: true,
initialScale: 1.0,
maxScale: 5.0,
animationMaxScale: 6.0,
initialAlignment: InitialAlignment.center,
);
},
);
},
GestureDetector(
onLongPress: () => onOpenMenu(),
child: ExtendedImageGesturePageView.builder(
controller: ExtendedPageController(
initialPage: _previewController.initialPage.value,
pageSpacing: 0,
),
onPageChanged: (int index) => _previewController.onChange(index),
canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0,
itemCount: widget.imgList!.length,
itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network(
widget.imgList![index],
fit: BoxFit.contain,
mode: ExtendedImageMode.gesture,
onDoubleTap: (ExtendedImageGestureState state) {
final Offset? pointerDownPosition =
state.pointerDownPosition;
final double? begin = state.gestureDetails!.totalScale;
double end;
//remove old
_doubleClickAnimation
?.removeListener(_doubleClickAnimationListener);
//stop pre
_doubleClickAnimationController.stop();
//reset to use
_doubleClickAnimationController.reset();
if (begin == doubleTapScales[0]) {
setState(() {
_dismissDisabled = true;
});
end = doubleTapScales[1];
} else {
setState(() {
_dismissDisabled = false;
});
end = doubleTapScales[0];
}
_doubleClickAnimationListener = () {
state.handleDoubleTap(
scale: _doubleClickAnimation!.value,
doubleTapPosition: pointerDownPosition);
};
_doubleClickAnimation = _doubleClickAnimationController
.drive(Tween<double>(begin: begin, end: end));
_doubleClickAnimation!
.addListener(_doubleClickAnimationListener);
_doubleClickAnimationController.forward();
},
// ignore: body_might_complete_normally_nullable
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =
state.loadingProgress;
final double? progress =
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 150.0,
child: LinearProgressIndicator(
value: progress,
color: Colors.white,
),
),
// const SizedBox(height: 10.0),
// Text('${((progress ?? 0.0) * 100).toInt()}%',),
],
),
);
}
},
initGestureConfigHandler: (ExtendedImageState state) {
return GestureConfig(
inPageView: true,
initialScale: 1.0,
maxScale: 5.0,
animationMaxScale: 6.0,
initialAlignment: InitialAlignment.center,
);
},
);
},
),
),
Positioned(
@ -251,33 +239,49 @@ class _ImagePreviewState extends State<ImagePreview>
right: 0,
bottom: 0,
child: Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black87,
padding: EdgeInsets.only(
left: 20,
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black87,
],
tileMode: TileMode.mirror,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.imgList!.length > 1
? Obx(
() => Text.rich(
textAlign: TextAlign.center,
TextSpan(
style: const TextStyle(
color: Colors.white, fontSize: 16),
children: [
TextSpan(
text: _previewController.currentPage
.toString()),
const TextSpan(text: ' / '),
TextSpan(
text:
widget.imgList!.length.toString()),
]),
),
)
: const SizedBox(),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close, color: Colors.white),
),
],
tileMode: TileMode.mirror,
),
),
child: Obx(
() => Text.rich(
textAlign: TextAlign.center,
TextSpan(
style: const TextStyle(color: Colors.white, fontSize: 15),
children: [
TextSpan(
text: _previewController.currentPage.toString()),
const TextSpan(text: ' / '),
TextSpan(text: widget.imgList!.length.toString()),
]),
),
),
),
)),
),
],
),

View File

@ -15,7 +15,7 @@ class SSearchController extends GetxController {
Box histiryWord = GStrorage.historyword;
List historyCacheList = [];
RxList historyList = [].obs;
RxList<SearchSuggestItem> searchSuggestList = [SearchSuggestItem()].obs;
RxList<SearchSuggestItem> searchSuggestList = <SearchSuggestItem>[].obs;
final _debouncer =
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
String hintText = '搜索';

View File

@ -8,6 +8,7 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/login.dart';
import 'package:pilipala/utils/storage.dart';
import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
import '../main/index.dart';
import 'widgets/select_dialog.dart';
@ -23,6 +24,7 @@ class SettingController extends GetxController {
Rx<ThemeType> themeType = ThemeType.system.obs;
var userInfo;
Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
RxInt defaultHomePage = 0.obs;
@override
void onInit() {
@ -40,6 +42,8 @@ class SettingController extends GetxController {
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
defaultHomePage.value =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0);
}
loginOut() async {
@ -110,4 +114,24 @@ class SettingController extends GetxController {
SmartDialog.showToast('设置成功');
}
}
// 设置默认启动页
seteDefaultHomePage(BuildContext context) async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(
title: '首页启动页',
value: defaultHomePage.value,
values: defaultNavigationBars.map((e) {
return {'title': e['label'], 'value': e['id']};
}).toList());
},
);
if (result != null) {
defaultHomePage.value = result;
setting.put(SettingBoxKey.defaultHomePage, result);
SmartDialog.showToast('设置成功,重启生效');
}
}
}

View File

@ -40,10 +40,6 @@ class _TabbarSetPageState extends State<TabbarSetPage> {
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.map<String>((i) => (i['type'] as TabType).id)
.toList();
if (sortedTabbar.isEmpty) {
SmartDialog.showToast('请至少设置一项!');
return;
}
settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar);
SmartDialog.showToast('保存成功,下次启动时生效');
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/global_data.dart';
import '../../../models/common/gesture_mode.dart';
import '../../../utils/storage.dart';
import '../widgets/select_dialog.dart';
import '../widgets/switch_item.dart';
class PlayGesturePage extends StatefulWidget {
const PlayGesturePage({super.key});
@override
State<PlayGesturePage> createState() => _PlayGesturePageState();
}
class _PlayGesturePageState extends State<PlayGesturePage> {
Box setting = GStrorage.setting;
late int fullScreenGestureMode;
@override
void initState() {
super.initState();
fullScreenGestureMode = setting.get(SettingBoxKey.fullScreenGestureMode,
defaultValue: FullScreenGestureMode.values.last.index);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'手势设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
ListTile(
dense: false,
title: Text('全屏手势', style: titleStyle),
subtitle: Text(
'通过手势快速进入全屏',
style: subTitleStyle,
),
onTap: () async {
String? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<String>(
title: '全屏手势',
value: FullScreenGestureMode
.values[fullScreenGestureMode].values,
values: FullScreenGestureMode.values.map((e) {
return {'title': e.labels, 'value': e.values};
}).toList());
},
);
if (result != null) {
GlobalData().fullScreenGestureMode = FullScreenGestureMode
.values
.firstWhere((element) => element.values == result);
fullScreenGestureMode =
GlobalData().fullScreenGestureMode.index;
setting.put(
SettingBoxKey.fullScreenGestureMode, fullScreenGestureMode);
setState(() {});
}
},
),
const SetSwitchItem(
title: '双击快退/快进',
subTitle: '左侧双击快退,右侧双击快进',
setKey: SettingBoxKey.enableQuickDouble,
defaultVal: true,
),
],
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
@ -73,6 +74,12 @@ class _PlaySettingState extends State<PlaySetting> {
title: Text('倍速设置', style: titleStyle),
subtitle: Text('设置视频播放速度', style: subTitleStyle),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/playerGestureSet'),
title: Text('手势设置', style: titleStyle),
subtitle: Text('设置播放器手势', style: subTitleStyle),
),
const SetSwitchItem(
title: '开启1080P',
subTitle: '免登录查看1080P视频',
@ -134,18 +141,20 @@ class _PlaySettingState extends State<PlaySetting> {
setKey: SettingBoxKey.enableAutoBrightness,
defaultVal: false,
),
const SetSwitchItem(
title: '双击快退/快进',
subTitle: '左侧双击快退,右侧双击快进',
setKey: SettingBoxKey.enableQuickDouble,
defaultVal: true,
),
const SetSwitchItem(
title: '弹幕开关',
subTitle: '展示弹幕',
setKey: SettingBoxKey.enableShowDanmaku,
defaultVal: false,
),
SetSwitchItem(
title: '控制栏动画',
subTitle: '播放器控制栏显示动画效果',
setKey: SettingBoxKey.enablePlayerControlAnimation,
defaultVal: true,
callFn: (bool val) {
GlobalData().enablePlayerControlAnimation = val;
}),
ListTile(
dense: false,
title: Text('默认画质', style: titleStyle),

View File

@ -12,6 +12,7 @@ import 'package:pilipala/utils/global_data.dart';
import 'package:pilipala/utils/storage.dart';
import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
import 'controller.dart';
import 'widgets/switch_item.dart';
@ -29,7 +30,6 @@ class _StyleSettingState extends State<StyleSetting> {
Box setting = GStrorage.setting;
late int picQuality;
late double toastOpacity;
late ThemeType _tempThemeValue;
late dynamic defaultCustomRows;
@ -37,7 +37,6 @@ class _StyleSettingState extends State<StyleSetting> {
void initState() {
super.initState();
picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
toastOpacity = setting.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0);
_tempThemeValue = settingController.themeType.value;
defaultCustomRows = setting.get(SettingBoxKey.customRows, defaultValue: 2);
}
@ -267,6 +266,14 @@ class _StyleSettingState extends State<StyleSetting> {
'当前主题:${colorSelectController.type.value == 0 ? '动态取色' : '指定颜色'}',
style: subTitleStyle)),
),
ListTile(
dense: false,
onTap: () => settingController.seteDefaultHomePage(context),
title: Text('默认启动页', style: titleStyle),
subtitle: Obx(() => Text(
'当前启动页:${defaultNavigationBars.firstWhere((e) => e['id'] == settingController.defaultHomePage.value)['label']}',
style: subTitleStyle)),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/fontSizeSetting'),

View File

@ -128,6 +128,7 @@ class VideoDetailController extends GetxController
controller: plPlayerController,
videoDetailCtr: this,
floating: floating,
bvid: bvid,
);
// CDN优化
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);

View File

@ -22,8 +22,9 @@ import '../related/index.dart';
import 'widgets/group_panel.dart';
class VideoIntroController extends GetxController {
VideoIntroController({required this.bvid});
// 视频bvid
String bvid = Get.parameters['bvid']!;
String bvid;
// 是否预渲染 骨架屏
bool preRender = false;

View File

@ -24,7 +24,10 @@ import 'widgets/page.dart';
import 'widgets/season.dart';
class VideoIntroPanel extends StatefulWidget {
const VideoIntroPanel({super.key});
final String bvid;
final String? cid;
const VideoIntroPanel({super.key, required this.bvid, this.cid});
@override
State<VideoIntroPanel> createState() => _VideoIntroPanelState();
@ -47,7 +50,8 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
/// fix 全屏时参数丢失
heroTag = Get.arguments['heroTag'];
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoIntroController =
Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag);
_futureBuilderFuture = videoIntroController.queryVideoIntro();
videoIntroController.videoDetail.listen((value) {
videoDetail = value;
@ -77,6 +81,7 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
loadingStatus: false,
videoDetail: videoIntroController.videoDetail.value,
heroTag: heroTag,
bvid: widget.bvid,
),
);
} else {
@ -95,6 +100,7 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
loadingStatus: true,
videoDetail: videoDetail,
heroTag: heroTag,
bvid: widget.bvid,
);
}
},
@ -106,10 +112,15 @@ class VideoInfo extends StatefulWidget {
final bool loadingStatus;
final VideoDetailData? videoDetail;
final String? heroTag;
final String bvid;
const VideoInfo(
{Key? key, this.loadingStatus = false, this.videoDetail, this.heroTag})
: super(key: key);
const VideoInfo({
Key? key,
this.loadingStatus = false,
this.videoDetail,
this.heroTag,
required this.bvid,
}) : super(key: key);
@override
State<VideoInfo> createState() => _VideoInfoState();
@ -149,7 +160,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
void initState() {
super.initState();
heroTag = widget.heroTag!;
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoIntroController =
Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoItem = videoIntroController.videoItem!;
sheetHeight = localCache.get('sheetHeight');

View File

@ -56,6 +56,37 @@ class _PagesPanelState extends State<PagesPanel> {
super.dispose();
}
Widget buildEpisodeListItem(
Part episode,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () {
changeFucCall(episode, index);
Get.back();
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
episode.pagePart!,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
@ -131,39 +162,25 @@ class _PagesPanelState extends State<PagesPanel> {
child: Material(
child: ListView.builder(
controller: _scrollController,
itemCount: episodes.length,
itemCount: episodes.length + 1,
itemBuilder:
(BuildContext context, int index) {
return ListTile(
onTap: () {
changeFucCall(
episodes[index], index);
Get.back();
},
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color: Theme.of(context)
.colorScheme
.primary,
height: 12,
)
: null,
title: Text(
episodes[index].pagePart!,
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface,
),
),
);
bool isLastItem =
index == episodes.length;
bool isCurrentIndex =
currentIndex == index;
return isLastItem
? SizedBox(
height: MediaQuery.of(context)
.padding
.bottom +
20,
)
: buildEpisodeListItem(
episodes[index],
index,
isCurrentIndex,
);
},
),
),
@ -192,6 +209,7 @@ class _PagesPanelState extends State<PagesPanel> {
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: (BuildContext context, int i) {
bool isCurrentIndex = currentIndex == i;
return Container(
width: 150,
margin: const EdgeInsets.only(right: 10),
@ -206,7 +224,7 @@ class _PagesPanelState extends State<PagesPanel> {
vertical: 8, horizontal: 8),
child: Row(
children: <Widget>[
if (i == currentIndex) ...<Widget>[
if (isCurrentIndex) ...<Widget>[
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
@ -220,7 +238,7 @@ class _PagesPanelState extends State<PagesPanel> {
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: i == currentIndex
color: isCurrentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,

View File

@ -80,6 +80,34 @@ class _SeasonPanelState extends State<SeasonPanel> {
super.dispose();
}
Widget buildEpisodeListItem(
EpisodeItem episode,
int index,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
return ListTile(
onTap: () => changeFucCall(episode, index),
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
episode.title!,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
);
}
@override
Widget build(BuildContext context) {
return Builder(builder: (BuildContext context) {
@ -134,32 +162,22 @@ class _SeasonPanelState extends State<SeasonPanel> {
child: Material(
child: ScrollablePositionedList.builder(
itemCount: episodes.length,
itemBuilder: (BuildContext context, int index) =>
ListTile(
onTap: () =>
changeFucCall(episodes[index], index),
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color: Theme.of(context)
.colorScheme
.primary,
height: 12,
itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == episodes.length - 1;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height: MediaQuery.of(context)
.padding
.bottom +
20,
)
: null,
title: Text(
episodes[index].title!,
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context)
.colorScheme
.onSurface,
),
),
),
: buildEpisodeListItem(
episodes[index],
index,
isCurrentIndex,
);
},
itemScrollController: itemScrollController,
),
),

View File

@ -22,7 +22,7 @@ class VideoReplyController extends GetxController {
String? replyLevel;
// rpid 请求楼中楼回复
String? rpid;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
// 当前页
int currentPage = 0;
bool isLoadingMore = false;
@ -62,6 +62,7 @@ class VideoReplyController extends GetxController {
noMore.value = '';
}
if (noMore.value == '没有更多了') {
isLoadingMore = false;
return;
}
final res = await ReplyHttp.replyList(

View File

@ -134,13 +134,13 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
super.build(context);
return RefreshIndicator(
onRefresh: () async {
_videoReplyController.currentPage = 0;
return await _videoReplyController.queryReplyList();
return await _videoReplyController.queryReplyList(type: 'init');
},
child: Stack(
children: [
CustomScrollView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
key: const PageStorageKey<String>('评论'),
slivers: <Widget>[
SliverPersistentHeader(

View File

@ -462,6 +462,9 @@ class ReplyItemRow extends StatelessWidget {
InlineSpan buildContent(
BuildContext context, replyItem, replyReply, fReplyItem) {
final String routePath = Get.currentRoute;
bool isVideoPage = routePath.startsWith('/video');
// replyItem 当前回复内容
// replyReply 查看二楼回复(回复详情)回调
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
@ -503,21 +506,25 @@ InlineSpan buildContent(
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
// print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString());
// 构建正则表达式
final List<String> specialTokens = [
...content.emote.keys,
...content.topicsMeta?.keys?.map((e) => '#$e#') ?? [],
...content.atNameToMid.keys.map((e) => '@$e'),
...content.jumpUrl.keys.map((e) =>
e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
];
List<dynamic> jumpUrlKeysList = content.jumpUrl.keys.map((e) {
return e.replaceAllMapped(
RegExp(r'[?+*]'), (match) => '\\${match.group(0)}');
}).toList();
String patternStr = specialTokens.map(RegExp.escape).join('|');
if (patternStr.isNotEmpty) {
patternStr += "|";
}
patternStr += r'(\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b)';
if (jumpUrlKeysList.isNotEmpty) {
patternStr += '|${jumpUrlKeysList.join('|')}';
}
final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = [];
void addPlainTextSpan(str) {
@ -571,27 +578,31 @@ InlineSpan buildContent(
spanChilds.add(
TextSpan(
text: ' $matchStr ',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
style: isVideoPage
? TextStyle(
color: Theme.of(context).colorScheme.primary,
)
: null,
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
);
} catch (e) {
SmartDialog.showToast('跳转失败: $e');
if (isVideoPage) {
try {
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
);
} catch (e) {
SmartDialog.showToast('跳转失败: $e');
}
}
},
),
);
} else {
print("matchStr=$matchStr");
String appUrlSchema = '';
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
defaultValue: false) as bool;

View File

@ -109,21 +109,26 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override
void didChangeMetrics() {
super.didChangeMetrics();
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
if (keyboardHeight == 0 && emoteHeight == 0) {
setState(() {
emoteHeight = keyboardHeight =
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
});
final String routePath = Get.currentRoute;
if (mounted &&
(routePath.startsWith('/video') ||
routePath.startsWith('/dynamicDetail'))) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
if (keyboardHeight == 0 && emoteHeight == 0) {
setState(() {
emoteHeight = keyboardHeight =
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
});
}
}
}
});
});
});
}
}
@override
@ -131,11 +136,15 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
WidgetsBinding.instance.removeObserver(this);
_replyContentController.dispose();
replyContentFocusNode.removeListener(() {});
replyContentFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom;
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(

View File

@ -12,7 +12,7 @@ class VideoReplyReplyController extends GetxController {
// rpid 请求楼中楼回复
String? rpid;
ReplyType replyType = ReplyType.video;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
// 当前页
int currentPage = 0;
bool isLoadingMore = false;

View File

@ -5,6 +5,7 @@ import 'dart:ui';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
@ -23,7 +24,6 @@ import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/plugin/pl_player/utils/fullscreen.dart';
import '../../../services/shutdown_timer_service.dart';
import 'widgets/header_control.dart';
@ -58,9 +58,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late bool autoExitFullcreen;
late bool autoPlayEnable;
late bool autoPiP;
final Floating floating = Floating();
// 生命周期监听
late final AppLifecycleListener _lifecycleListener;
late Floating floating;
bool isShowing = true;
@override
@ -68,7 +66,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
super.initState();
heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoIntroController = Get.put(
VideoIntroController(bvid: Get.parameters['bvid']!),
tag: heroTag);
videoIntroController.videoDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
@ -91,8 +91,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoSourceInit();
appbarStreamListen();
lifecycleListener();
fullScreenStatusListener();
if (Platform.isAndroid) {
floating = videoDetailController.floating!;
autoEnterPip();
}
}
// 获取视频资源,初始化播放器
@ -150,6 +153,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
} catch (_) {}
}
if (Platform.isAndroid) {
floating.toggleAutoPip(
autoEnter: status == PlayerStatus.playing && autoPiP);
}
}
// 继续播放或重新播放
@ -168,27 +175,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoDetailController.isShowCover.value = false;
}
// 生命周期监听
void lifecycleListener() {
_lifecycleListener = AppLifecycleListener(
onResume: () => _handleTransition('resume'),
// 后台
onInactive: () => _handleTransition('inactive'),
// 在Android和iOS端不生效
onHide: () => _handleTransition('hide'),
onShow: () => _handleTransition('show'),
onPause: () => _handleTransition('pause'),
onRestart: () => _handleTransition('restart'),
onDetach: () => _handleTransition('detach'),
// 只作用于桌面端
onExitRequested: () {
ScaffoldMessenger.maybeOf(context)
?.showSnackBar(const SnackBar(content: Text("拦截应用退出")));
return Future.value(AppExitResponse.cancel);
},
);
}
void fullScreenStatusListener() {
plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) {
@ -208,8 +194,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoDetailController.floating!.dispose();
}
videoPlayerServiceHandler.onVideoDetailDispose();
floating.dispose();
_lifecycleListener.dispose();
if (Platform.isAndroid) {
floating.toggleAutoPip(autoEnter: false);
floating.dispose();
}
super.dispose();
}
@ -262,29 +250,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.subscribe(this, ModalRoute.of(context)! as PageRoute);
}
void _handleTransition(String name) {
switch (name) {
case 'inactive':
if (plPlayerController != null &&
playerStatus == PlayerStatus.playing) {
autoEnterPip();
}
break;
}
}
void autoEnterPip() {
final String routePath = Get.currentRoute;
final bool isPortrait =
MediaQuery.of(context).orientation == Orientation.portrait;
/// TODO 横屏全屏状态下误触pip
if (autoPiP && routePath.startsWith('/video') && isPortrait) {
floating.enable(
aspectRatio: Rational(
videoDetailController.data.dash!.video!.first.width!,
videoDetailController.data.dash!.video!.first.height!,
));
if (autoPiP && routePath.startsWith('/video')) {
floating.toggleAutoPip(autoEnter: autoPiP);
}
}
@ -404,7 +373,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
);
} else {
return const SizedBox();
return buildCustomAppBar();
}
},
),
@ -447,33 +416,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
top: 0,
left: 0,
right: 0,
child: AppBar(
primary: false,
foregroundColor:
Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor:
Colors.transparent,
actions: [
IconButton(
tooltip: '稍后再看',
onPressed: () async {
var res = await UserHttp
.toViewLater(
bvid: videoDetailController
.bvid);
SmartDialog
.showToast(
res['msg']);
},
icon: const Icon(Icons
.history_outlined),
),
const SizedBox(
width: 14)
],
),
child: buildCustomAppBar(),
),
Positioned(
right: 12,
@ -550,7 +493,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
slivers: <Widget>[
if (videoDetailController.videoType ==
SearchType.video) ...[
const VideoIntroPanel(),
VideoIntroPanel(
bvid: videoDetailController.bvid),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[
Obx(() => BangumiIntroPanel(
@ -577,7 +521,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.withOpacity(0.06),
),
),
RelatedVideoPanel(),
const RelatedVideoPanel(),
],
);
},
@ -627,6 +571,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
headerControl: HeaderControl(
controller: plPlayerController,
videoDetailCtr: videoDetailController,
bvid: videoDetailController.bvid,
),
danmuWidget: Obx(
() => PlDanmaku(
@ -653,4 +598,48 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return childWhenDisabled;
}
}
Widget buildCustomAppBar() {
return AppBar(
backgroundColor: Colors.transparent, // 使背景透明
foregroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
centerTitle: false,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Container(
height: kToolbarHeight,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
)),
child: Row(
children: [
ComBtn(
icon: const Icon(FontAwesomeIcons.arrowLeft, size: 15),
fuc: () => Get.back(),
),
const Spacer(),
ComBtn(
icon: const Icon(Icons.history_outlined, size: 22),
fuc: () async {
var res = await UserHttp.toViewLater(
bvid: videoDetailController.bvid);
SmartDialog.showToast(res['msg']);
},
),
],
),
),
);
}
}

View File

@ -27,11 +27,13 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
this.controller,
this.videoDetailCtr,
this.floating,
this.bvid,
super.key,
});
final PlPlayerController? controller;
final VideoDetailController? videoDetailCtr;
final Floating? floating;
final String? bvid;
@override
State<HeaderControl> createState() => _HeaderControlState();
@ -62,7 +64,8 @@ class _HeaderControlState extends State<HeaderControl> {
speedsList = widget.controller!.speedsList;
fullScreenStatusListener();
heroTag = Get.arguments['heroTag'];
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoIntroController =
Get.put(VideoIntroController(bvid: widget.bvid!), tag: heroTag);
}
void fullScreenStatusListener() {

View File

@ -131,13 +131,13 @@ class WebviewController extends GetxController {
Get.back();
} else {
// 获取用户信息失败
SmartDialog.showToast(result.msg);
Clipboard.setData(ClipboardData(text: result.msg.toString()));
SmartDialog.showToast(result['msg']);
Clipboard.setData(ClipboardData(text: result['msg']));
}
} catch (e) {
SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning);
content = content + e.toString();
Clipboard.setData(ClipboardData(text: content));
}
Clipboard.setData(ClipboardData(text: content));
}
}

View File

@ -51,27 +51,31 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
@override
void didChangeMetrics() {
super.didChangeMetrics();
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
if (keyboardHeight == 0) {
setState(() {
emoteHeight = keyboardHeight =
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
});
final String routePath = Get.currentRoute;
if (mounted && routePath.startsWith('/whisper_detail')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
if (keyboardHeight == 0) {
setState(() {
emoteHeight = keyboardHeight =
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
});
}
}
}
});
});
});
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
replyContentFocusNode.removeListener(() {});
replyContentFocusNode.dispose();
super.dispose();
}

View File

@ -9,6 +9,7 @@ import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/models/common/gesture_mode.dart';
import 'package:pilipala/plugin/pl_player/controller.dart';
import 'package:pilipala/plugin/pl_player/models/duration.dart';
import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';
@ -17,6 +18,7 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import '../../utils/global_data.dart';
import 'models/bottom_progress_behavior.dart';
import 'widgets/app_bar_ani.dart';
import 'widgets/backward_seek.dart';
@ -73,6 +75,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late bool enableQuickDouble;
late bool enableBackgroundPlay;
late double screenWidth;
final FullScreenGestureMode fullScreenGestureMode =
GlobalData().fullScreenGestureMode;
// 用于记录上一次全屏切换手势触发时间,避免误触
DateTime? lastFullScreenToggleTime;
@ -116,7 +120,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
super.initState();
screenWidth = Get.size.width;
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
vsync: this,
duration: GlobalData().enablePlayerControlAnimation
? const Duration(milliseconds: 150)
: const Duration(milliseconds: 10),
);
videoController = widget.controller.videoController!;
widget.controller.headerControl = widget.headerControl;
widget.controller.bottomControl = widget.bottomControl;
@ -520,18 +528,20 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 全屏
final double dy = details.delta.dy;
const double threshold = 7.0; // 滑动阈值
final bool flag =
fullScreenGestureMode != FullScreenGestureMode.values.last;
if (dy > _distance && dy > threshold) {
if (_.isFullScreen.value) {
if (_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 下滑退出全屏
await widget.controller.triggerFullScreen(status: false);
await widget.controller.triggerFullScreen(status: flag);
}
_distance = 0.0;
} else if (dy < _distance && dy < -threshold) {
if (!_.isFullScreen.value) {
if (!_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 上滑进入全屏
await widget.controller.triggerFullScreen();
await widget.controller.triggerFullScreen(status: !flag);
}
_distance = 0.0;
}

View File

@ -19,11 +19,11 @@ class AppBarAni extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
visible ? controller.reverse() : controller.forward();
visible ? controller.forward() : controller.reverse();
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: Offset(0, position! == 'top' ? -1 : 1),
begin: Offset(0, position! == 'top' ? -1 : 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.linear,

View File

@ -39,6 +39,7 @@ import '../pages/setting/pages/color_select.dart';
import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart';
import '../pages/setting/pages/home_tabbar_set.dart';
import '../pages/setting/pages/play_gesture_set.dart';
import '../pages/setting/pages/play_speed_set.dart';
import '../pages/setting/recommend_setting.dart';
import '../pages/setting/play_setting.dart';
@ -166,6 +167,9 @@ class Routes {
CustomGetPage(name: '/subscription', page: () => const SubPage()),
// 订阅详情
CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()),
// 播放器手势
CustomGetPage(
name: '/playerGestureSet', page: () => const PlayGesturePage()),
];
}

View File

@ -149,6 +149,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
));
if (_item.isNotEmpty) {
_item.removeLast();
}
if (_item.isNotEmpty) {
setMediaItem(_item.last);
}
if (_item.isEmpty) {

View File

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import '../http/search.dart';
import '../models/common/search_type.dart';
import 'id_utils.dart';
import 'url_utils.dart';
import 'utils.dart';
class PiliSchame {
@ -38,23 +39,16 @@ class PiliSchame {
final String path = value.path;
if (scheme == 'bilibili') {
// bilibili://root
if (host == 'root') {
Navigator.popUntil(
Get.context!, (Route<dynamic> route) => route.isFirst);
}
// bilibili://space/{uid}
else if (host == 'space') {
} else if (host == 'space') {
final String mid = path.split('/').last;
Get.toNamed<dynamic>(
'/member?mid=$mid',
arguments: <String, dynamic>{'face': null},
);
}
// bilibili://video/{aid}
else if (host == 'video') {
} else if (host == 'video') {
String pathQuery = path.split('/').last;
final numericRegex = RegExp(r'^[0-9]+$');
if (numericRegex.hasMatch(pathQuery)) {
@ -68,24 +62,16 @@ class PiliSchame {
} else {
SmartDialog.showToast('投稿匹配失败');
}
}
// bilibili://live/{roomid}
else if (host == 'live') {
} else if (host == 'live') {
final String roomId = path.split('/').last;
Get.toNamed<dynamic>('/liveRoom?roomid=$roomId',
arguments: <String, String?>{'liveItem': null, 'heroTag': roomId});
}
// bilibili://bangumi/season/${ssid}
else if (host == 'bangumi') {
} else if (host == 'bangumi') {
if (path.startsWith('/season')) {
final String seasonId = path.split('/').last;
_bangumiPush(int.parse(seasonId));
_bangumiPush(int.parse(seasonId), null);
}
}
// 专栏 bilibili://opus/detail/883089655985078289
else if (host == 'opus') {
} else if (host == 'opus') {
if (path.startsWith('/detail')) {
var opusId = path.split('/').last;
Get.toNamed(
@ -101,6 +87,9 @@ class PiliSchame {
Get.toNamed('/searchResult', parameters: {'keyword': ''});
}
}
if (scheme == 'https') {
_fullPathPush(value);
}
}
// 投稿跳转
@ -131,10 +120,10 @@ class PiliSchame {
}
// 番剧跳转
static Future<void> _bangumiPush(int seasonId) async {
static Future<void> _bangumiPush(int? seasonId, int? epId) async {
SmartDialog.showLoading<dynamic>(msg: '获取中...');
try {
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: null);
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) {
var bangumiDetail = result['data'];
final int cid = bangumiDetail.episodes!.first.cid;
@ -151,6 +140,8 @@ class PiliSchame {
},
),
);
} else {
SmartDialog.showToast(result['msg']);
}
} catch (e) {
SmartDialog.showToast('番剧获取失败:$e');
@ -163,29 +154,67 @@ class PiliSchame {
// final String scheme = value.scheme!;
final String host = value.host!;
final String? path = value.path;
// Map<String, String> query = value.query!;
if (host.startsWith('live.bilibili')) {
Map<String, String>? query = value.query;
RegExp regExp = RegExp(r'^(www\.)?m?\.(bilibili\.com)$');
if (regExp.hasMatch(host)) {
print('bilibili.com');
} else if (host.contains('live')) {
int roomId = int.parse(path!.split('/').last);
// print('直播');
Get.toNamed('/liveRoom?roomid=$roomId',
arguments: {'liveItem': null, 'heroTag': roomId.toString()});
return;
}
if (host.startsWith('space.bilibili')) {
print('个人空间');
Get.toNamed(
'/liveRoom?roomid=$roomId',
arguments: {'liveItem': null, 'heroTag': roomId.toString()},
);
} else if (host.contains('space')) {
var mid = path!.split('/').last;
Get.toNamed('/member?mid=$mid', arguments: {'face': ''});
return;
} else if (host == 'b23.tv') {
final String fullPath = 'https://$host$path';
final String redirectUrl = await UrlUtils.parseRedirectUrl(fullPath);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment = pathSegment.split('/').last;
final RegExp avRegex = RegExp(r'^[aA][vV]\d+', caseSensitive: false);
if (avRegex.hasMatch(lastPathSegment)) {
final Map<String, dynamic> map =
IdUtils.matchAvorBv(input: lastPathSegment);
if (map.containsKey('AV')) {
_videoPush(map['AV']! as int, null);
} else if (map.containsKey('BV')) {
_videoPush(null, map['BV'] as String);
} else {
SmartDialog.showToast('投稿匹配失败');
}
} else if (lastPathSegment.startsWith('ep')) {
_handleEpisodePath(lastPathSegment, redirectUrl);
} else if (lastPathSegment.startsWith('ss')) {
_handleSeasonPath(lastPathSegment, redirectUrl);
} else if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
'',
redirectUrl,
);
} else {
Get.toNamed(
'/webview',
parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''},
);
}
}
if (path != null) {
final String area = path.split('/')[1];
final String area = path.split('/').last;
switch (area) {
case 'bangumi':
// print('番剧');
final String seasonId = path.split('/').last;
_bangumiPush(matchNum(seasonId).first);
print('番剧');
if (area.startsWith('ep')) {
_bangumiPush(null, matchNum(area).first);
} else if (area.startsWith('ss')) {
_bangumiPush(matchNum(area).first, null);
}
break;
case 'video':
// print('投稿');
print('投稿');
final Map<String, dynamic> map = IdUtils.matchAvorBv(input: path);
if (map.containsKey('AV')) {
_videoPush(map['AV']! as int, null);
@ -200,6 +229,7 @@ class PiliSchame {
break;
case 'space':
print('个人空间');
Get.toNamed('/member?mid=$area', arguments: {'face': ''});
break;
}
}
@ -211,4 +241,18 @@ class PiliSchame {
return matches.map((Match match) => int.parse(match.group(0)!)).toList();
}
static void _handleEpisodePath(String lastPathSegment, String redirectUrl) {
final String seasonId = _extractIdFromPath(lastPathSegment);
_bangumiPush(null, matchNum(seasonId).first);
}
static void _handleSeasonPath(String lastPathSegment, String redirectUrl) {
final String seasonId = _extractIdFromPath(lastPathSegment);
_bangumiPush(matchNum(seasonId).first, null);
}
static String _extractIdFromPath(String lastPathSegment) {
return lastPathSegment.split('/').last;
}
}

View File

@ -1,5 +1,10 @@
import '../models/common/index.dart';
class GlobalData {
int imgQuality = 10;
FullScreenGestureMode fullScreenGestureMode =
FullScreenGestureMode.values.last;
bool enablePlayerControlAnimation = true;
// 私有构造函数
GlobalData._();

View File

@ -68,8 +68,9 @@ class IdUtils {
if (input == null || input.isEmpty) {
return result;
}
final RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
final RegExp avRegex = RegExp(r'AV\d+', caseSensitive: false);
final RegExp bvRegex =
RegExp(r'[bB][vV][0-9A-Za-z]{10}', caseSensitive: false);
final RegExp avRegex = RegExp(r'[aA][vV]\d+', caseSensitive: false);
final Iterable<Match> bvMatches = bvRegex.allMatches(input);
final Iterable<Match> avMatches = avRegex.allMatches(input);

View File

@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:pilipala/models/model_owner.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart';
import '../models/common/gesture_mode.dart';
import 'global_data.dart';
class GStrorage {
@ -45,6 +46,11 @@ class GStrorage {
video = await Hive.openBox('video');
GlobalData().imgQuality =
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); // 设置全局变量
GlobalData().fullScreenGestureMode = FullScreenGestureMode.values[
setting.get(SettingBoxKey.fullScreenGestureMode,
defaultValue: FullScreenGestureMode.values.last.index) as int];
GlobalData().enablePlayerControlAnimation = setting
.get(SettingBoxKey.enablePlayerControlAnimation, defaultValue: true);
}
static void regAdapter() {
@ -94,11 +100,13 @@ class SettingBoxKey {
enableCDN = 'enableCDN',
autoPiP = 'autoPiP',
enableAutoLongPressSpeed = 'enableAutoLongPressSpeed',
enablePlayerControlAnimation = 'enablePlayerControlAnimation',
// youtube 双击快进快退
enableQuickDouble = 'enableQuickDouble',
enableShowDanmaku = 'enableShowDanmaku',
enableBackgroundPlay = 'enableBackgroundPlay',
fullScreenGestureMode = 'fullScreenGestureMode',
/// 隐私
blackMidsList = 'blackMidsList',
@ -122,7 +130,8 @@ class SettingBoxKey {
enableWordRe = 'enableWordRe',
enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi';
enableAi = 'enableAi',
defaultHomePage = 'defaultHomePage';
/// 外观
static const String themeMode = 'themeMode',

View File

@ -502,7 +502,7 @@ packages:
description:
path: "."
ref: main
resolved-ref: d2d8421c4d80f6113f832404109853684721e11a
resolved-ref: "8e89669eb9341f9980265306e24ef96fdbd3fd08"
url: "https://github.com/guozhigq/floating.git"
source: git
version: "2.0.1"