Compare commits

...

49 Commits

Author SHA1 Message Date
99e6abdad9 feat: 相关视频推荐开关 issues #585 2024-03-11 23:31:20 +08:00
3ece2bb173 feat: 系统倍速可编辑 2024-03-10 17:57:24 +08:00
06fb3e8d2f fix: 请求github异常 2024-03-09 01:25:54 +08:00
504be6fbda fix: 搜索结果类型为课堂时渲染异常 2024-03-09 01:18:26 +08:00
df4539a035 fix: github链接 issues #618 2024-03-08 23:15:48 +08:00
a3e1fd4e91 fix: 清除缓存提示 issues #619 2024-03-08 23:09:52 +08:00
3bf6136bc6 fix: 楼中楼评论请求重复 #284 2024-03-08 00:03:34 +08:00
ab24da5f55 fix: 媒体通知进度条未按预期停止 2024-03-07 23:35:39 +08:00
ed0b43eff1 v1.0.21 更新日志 2024-03-06 23:29:18 +08:00
ab9ae3a481 fix: setState() called after dispose() 导致全屏失效 2024-03-06 00:04:52 +08:00
d728b1fb6d mod: 评论区非正常地址判断 2024-03-05 23:39:05 +08:00
12e947ef84 fix: reply callback null error issues #615 2024-03-05 23:21:51 +08:00
3fad86e7e3 fix: 视频简介被遮挡 issues #613 2024-03-05 23:04:59 +08:00
fea70011cb fix: navBars unmodifiable issues #612 2024-03-05 23:01:23 +08:00
32cdb27f7c fix: enableGradientBg未定义 2024-03-05 22:37:52 +08:00
eb4435045b fix: 番剧全屏时title取值异常 2024-03-04 23:48:01 +08:00
f1b829cec1 fix: 首页tarbar指示器跳动 2024-03-04 08:29:01 +08:00
b248158e62 v1.0.20 更新日志 2024-03-03 19:48:10 +08:00
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
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
45 changed files with 1036 additions and 609 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>

31
change_log/1.0.20.0303.md Normal file
View File

@ -0,0 +1,31 @@
## 1.0.20
### 功能
+ 评论区增加表情
+ 首页渐变背景开关
+ 媒体库显示「我的订阅」
+ 评论区链接解析
+ 默认启动页设置
### 修复
+ 评论区内容重复
+ pip相关问题
+ 播放多p视频评论不刷新
+ 视频评论翻页重复
### 优化
+ url scheme优化
+ 图片预览放大
+ 图片加载速度
+ 视频评论区复制
+ 全屏显示视频标题
+ 网络异常处理
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -0,0 +1,9 @@
## 1.0.21
### 修复
+ 推荐视频全屏问题
+ 番剧全屏播放时灰屏问题
+ 评论回调导致页面卡死问题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -38,6 +38,10 @@ class VideoCardH extends StatelessWidget {
Widget build(BuildContext context) {
final int aid = videoItem.aid;
final String bvid = videoItem.bvid;
String type = 'video';
try {
type = videoItem.type;
} catch (_) {}
final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector(
onLongPress: () {
@ -53,6 +57,10 @@ class VideoCardH extends StatelessWidget {
child: InkWell(
onTap: () async {
try {
if (type == 'ketang') {
SmartDialog.showToast('课堂视频暂不支持播放');
return;
}
final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
@ -95,12 +103,20 @@ class VideoCardH extends StatelessWidget {
height: maxHeight,
),
),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (videoItem.duration != 0)
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (type != 'video')
PBadge(
text: type,
left: 6.0,
bottom: 6.0,
type: 'primary',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,

View File

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

View File

@ -3,11 +3,13 @@ class 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
@ -21,6 +23,7 @@ class FollowUpModel {
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;
}
}
@ -100,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

@ -85,7 +85,9 @@ class SearchVideoItemModel {
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
description = json['description'];
pic = 'https:${json['pic']}';
pic = json['pic'] != null && json['pic'].startsWith('//')
? 'https:${json['pic']}'
: json['pic'] ?? '';
videoReview = json['video_review'];
pubdate = json['pubdate'];
senddate = json['senddate'];

View File

@ -201,7 +201,6 @@ class _AboutPageState extends State<AboutPage> {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
SmartDialog.showToast('清除成功');
}
},
title: const Text('清除缓存'),
@ -254,12 +253,16 @@ class AboutController extends GetxController {
// 获取远程版本
Future getRemoteApp() async {
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
isLoading.value = false;
if (result.data == null || result.data.isEmpty) {
SmartDialog.showToast('获取远程版本失败,请检查网络');
return;
}
data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data;
remoteVersion.value = data.tagName!;
isUpdate.value =
Utils.needUpdate(currentVersion.value, remoteVersion.value);
isLoading.value = false;
}
// 跳转下载/本地更新
@ -277,7 +280,7 @@ class AboutController extends GetxController {
githubRelease() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/release'),
Uri.parse('https://github.com/guozhigq/pilipala/releases'),
mode: LaunchMode.externalApplication,
);
}

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

@ -258,6 +258,10 @@ 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;
}

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

@ -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,33 +22,17 @@ 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!.liveList!.isNotEmpty) {
liveList = widget.upData!.liveList!;
}
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,

View File

@ -34,8 +34,6 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
userFace.value = userInfo != null ? userInfo.face : '';
// 进行tabs配置
setTabConfig();
hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
@ -43,6 +41,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
}
enableGradientBg =
setting.get(SettingBoxKey.enableGradientBg, defaultValue: true);
// 进行tabs配置
setTabConfig();
}
void onRefresh() {
@ -91,19 +91,21 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
vsync: this,
);
// 监听 tabController 切换
tabController.animation!.addListener(() {
if (tabController.indexIsChanging) {
if (initialIndex.value != tabController.index) {
initialIndex.value = tabController.index;
if (enableGradientBg) {
tabController.animation!.addListener(() {
if (tabController.indexIsChanging) {
if (initialIndex.value != tabController.index) {
initialIndex.value = tabController.index;
}
} else {
final int temp = tabController.animation!.value.round();
if (initialIndex.value != temp) {
initialIndex.value = temp;
tabController.index = initialIndex.value;
}
}
} else {
final int temp = tabController.animation!.value.round();
if (initialIndex.value != temp) {
initialIndex.value = temp;
tabController.index = initialIndex.value;
}
}
});
});
}
}
void searchDefault() async {

View File

@ -29,7 +29,6 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget {
class _BottomControlState extends State<BottomControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);

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

@ -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

@ -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

@ -173,6 +173,12 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableAi,
defaultVal: true,
),
const SetSwitchItem(
title: '相关视频推荐',
subTitle: '视频详情页推荐相关视频',
setKey: SettingBoxKey.enableRelatedVideo,
defaultVal: true,
),
ListTile(
dense: false,
title: Text('评论展示', style: titleStyle),

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

@ -17,6 +17,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
Box videoStorage = GStrorage.video;
Box settingStorage = GStrorage.setting;
late double playSpeedDefault;
late List<double> playSpeedSystem;
late double longPressSpeedDefault;
late List customSpeedsList;
late bool enableAutoLongPressSpeed;
@ -53,6 +54,9 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
@override
void initState() {
super.initState();
// 系统预设倍速
playSpeedSystem =
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
// 默认倍速
playSpeedDefault =
videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
@ -64,6 +68,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []);
enableAutoLongPressSpeed = settingStorage
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
// 开启动态长按倍速时不展示
if (enableAutoLongPressSpeed) {
Map newItem = sheetMenu[1];
newItem['show'] = false;
@ -123,7 +128,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
}
// 设定倍速弹窗
void showBottomSheet(type, i) {
void showBottomSheet(String type, int i) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
@ -159,18 +164,11 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
}
//
void menuAction(type, index, id) async {
void menuAction(type, int index, id) async {
double chooseSpeed = 1.0;
if (type == 'system' && id == -1) {
SmartDialog.showToast('系统预设倍速不支持删除');
return;
}
// 获取当前选中的倍速值
if (type == 'system') {
chooseSpeed = PlaySpeed.values[index].value;
} else {
chooseSpeed = customSpeedsList[index];
}
chooseSpeed =
type == 'system' ? playSpeedSystem[index] : customSpeedsList[index];
// 设置
if (id == 1) {
// 设置默认倍速
@ -182,17 +180,22 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
videoStorage.put(
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
} else if (id == -1) {
if (customSpeedsList[index] == playSpeedDefault) {
playSpeedDefault = 1.0;
videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault);
late List speedsList =
type == 'system' ? playSpeedSystem : customSpeedsList;
if (speedsList[index] == playSpeedDefault) {
SmartDialog.showToast('默认倍速不可删除');
}
if (customSpeedsList[index] == longPressSpeedDefault) {
if (speedsList[index] == longPressSpeedDefault) {
longPressSpeedDefault = 2.0;
videoStorage.put(
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
}
customSpeedsList.removeAt(index);
await videoStorage.put(VideoBoxKey.customSpeedsList, customSpeedsList);
speedsList.removeAt(index);
await videoStorage.put(
type == 'system'
? VideoBoxKey.playSpeedSystem
: VideoBoxKey.customSpeedsList,
speedsList);
}
setState(() {});
SmartDialog.showToast('操作成功');
@ -249,38 +252,40 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
subtitle: Text(longPressSpeedDefault.toString()),
)
: const SizedBox(),
Padding(
padding: const EdgeInsets.only(
left: 14,
right: 14,
bottom: 10,
top: 20,
if (playSpeedSystem.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(
left: 14,
right: 14,
bottom: 10,
top: 20,
),
child: Text(
'系统预设倍速',
style: Theme.of(context).textTheme.titleMedium,
),
),
child: Text(
'系统预设倍速',
style: Theme.of(context).textTheme.titleMedium,
),
),
Padding(
padding: const EdgeInsets.only(
left: 18,
right: 18,
bottom: 30,
),
child: Wrap(
alignment: WrapAlignment.start,
spacing: 8,
runSpacing: 2,
children: [
for (var i in PlaySpeed.values) ...[
FilledButton.tonal(
onPressed: () => showBottomSheet('system', i.index),
child: Text(i.description),
),
]
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 18,
right: 18,
bottom: 30,
),
child: Wrap(
alignment: WrapAlignment.start,
spacing: 8,
runSpacing: 2,
children: [
for (int i = 0; i < playSpeedSystem.length; i++) ...[
FilledButton.tonal(
onPressed: () => showBottomSheet('system', i),
child: Text(playSpeedSystem[i].toString()),
),
]
],
),
)
],
Padding(
padding: const EdgeInsets.only(
left: 14,

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

@ -91,6 +91,7 @@ class VideoDetailController extends GetxController
late int cacheAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr;
late bool enableRelatedVideo;
@override
void onInit() {
@ -113,7 +114,8 @@ class VideoDetailController extends GetxController
autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
enableRelatedVideo =
setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);
if (userInfo == null ||
localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
@ -129,6 +131,7 @@ class VideoDetailController extends GetxController
videoDetailCtr: this,
floating: floating,
bvid: bvid,
videoType: videoType,
);
// CDN优化
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);

View File

@ -23,7 +23,10 @@ class IntroDetail extends StatelessWidget {
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(left: 14, right: 14),
padding: EdgeInsets.only(
left: 14,
right: 14,
bottom: MediaQuery.of(context).padding.bottom + 20),
height: sheetHeight,
child: Column(
children: [

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

@ -280,7 +280,7 @@ class ReplyItem extends StatelessWidget {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
addReply!(value['data'])
addReply?.call(value['data'])
// replyControl.replies.add(value['data']),
}
});
@ -531,8 +531,8 @@ InlineSpan buildContent(
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap =
() => replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem)));
}
// 分割文本并处理每个部分
@ -642,6 +642,11 @@ InlineSpan buildContent(
} else {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
if (redirectUrl == matchStr) {
Clipboard.setData(ClipboardData(text: matchStr));
SmartDialog.showToast('地址可能有误');
return;
}
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;

View File

@ -30,6 +30,9 @@ class VideoReplyReplyController extends GetxController {
if (type == 'init') {
currentPage = 0;
}
if (isLoadingMore) {
return;
}
isLoadingMore = true;
final res = await ReplyHttp.replyReplyList(
oid: aid!,
@ -41,7 +44,7 @@ class VideoReplyReplyController extends GetxController {
final List<ReplyItemModel> replies = res['data'].replies;
if (replies.isNotEmpty) {
noMore.value = '加载中...';
if (replyList.length == res['data'].page.count) {
if (replies.length == res['data'].page.count) {
noMore.value = '没有更多了';
}
currentPage++;
@ -50,21 +53,6 @@ class VideoReplyReplyController extends GetxController {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
}
if (type == 'init') {
// List<ReplyItemModel> replies = res['data'].replies;
// 添加置顶回复
// if (res['data'].upper.top != null) {
// bool flag = false;
// for (var i = 0; i < res['data'].topReplies.length; i++) {
// if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) {
// flag = true;
// }
// }
// if (!flag) {
// replies.insert(0, res['data'].upper.top);
// }
// }
// replies.insertAll(0, res['data'].topReplies);
// res['data'].replies = replies;
replyList.value = replies;
} else {
// 每次回复之后,翻页请求有且只有相同的一条回复数据

View File

@ -54,7 +54,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
() {
_videoReplyReplyController.queryReplyList(type: 'onLoad');
});
}
@ -92,7 +93,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
icon: const Icon(Icons.close, size: 20),
onPressed: () {
_videoReplyReplyController.currentPage = 0;
widget.closePanel!();
widget.closePanel?.call;
Navigator.pop(context);
},
),
@ -184,6 +185,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
.add(replyItem);
},
replyType: widget.replyType,
replyReply: (replyItem) =>
replyReply(replyItem),
);
}
},

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';
@ -57,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
@ -92,8 +91,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoSourceInit();
appbarStreamListen();
lifecycleListener();
fullScreenStatusListener();
if (Platform.isAndroid) {
floating = videoDetailController.floating!;
autoEnterPip();
}
}
// 获取视频资源,初始化播放器
@ -151,6 +153,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
} catch (_) {}
}
if (Platform.isAndroid) {
floating.toggleAutoPip(
autoEnter: status == PlayerStatus.playing && autoPiP);
}
}
// 继续播放或重新播放
@ -169,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) {
@ -209,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();
}
@ -228,7 +215,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause();
}
print('🐶🐶');
setState(() => isShowing = false);
super.didPushNext();
}
@ -264,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);
}
}
@ -406,7 +373,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
);
} else {
return const SizedBox();
return buildCustomAppBar();
}
},
),
@ -449,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,
@ -580,7 +521,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.withOpacity(0.06),
),
),
const RelatedVideoPanel(),
if (videoDetailController.videoType ==
SearchType.video &&
videoDetailController.enableRelatedVideo)
const RelatedVideoPanel(),
],
);
},
@ -631,6 +575,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
controller: plPlayerController,
videoDetailCtr: videoDetailController,
bvid: videoDetailController.bvid,
videoType: videoDetailController.videoType,
),
danmuWidget: Obx(
() => PlDanmaku(
@ -657,4 +602,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

@ -19,6 +19,7 @@ import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/http/danmaku.dart';
import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart';
import '../introduction/index.dart';
@ -28,12 +29,14 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
this.videoDetailCtr,
this.floating,
this.bvid,
this.videoType,
super.key,
});
final PlPlayerController? controller;
final VideoDetailController? videoDetailCtr;
final Floating? floating;
final String? bvid;
final SearchType? videoType;
@override
State<HeaderControl> createState() => _HeaderControlState();
@ -44,7 +47,6 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
static const TextStyle subTitleStyle = TextStyle(fontSize: 12);
static const TextStyle titleStyle = TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@ -76,7 +78,11 @@ class _HeaderControlState extends State<HeaderControl> {
} else {
showTitle = false;
}
setState(() {});
/// TODO setState() called after dispose()
if (mounted) {
setState(() {});
}
});
}
@ -1107,14 +1113,16 @@ class _HeaderControlState extends State<HeaderControl> {
},
),
SizedBox(width: buttonSpace),
if (showTitle && isLandscape) ...[
if (showTitle &&
isLandscape &&
widget.videoType == SearchType.video) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 200),
constraints: const BoxConstraints(maxWidth: 200),
child: Text(
videoIntroController.videoDetail.value.title!,
videoIntroController.videoDetail.value.title ?? '',
style: const TextStyle(
color: Colors.white,
fontSize: 16,

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

@ -277,8 +277,7 @@ class PlPlayerController {
danmakuDurationVal =
localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
// 描边粗细
strokeWidth =
localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
strokeWidth = localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
playRepeat = PlayRepeat.values.toList().firstWhere(
(e) =>
e.value ==
@ -293,11 +292,19 @@ class PlPlayerController {
_longPressSpeed.value = videoStorage
.get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0);
}
// 自定义倍速集合
speedsList = List<double>.from(videoStorage
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
for (final PlaySpeed i in PlaySpeed.values) {
speedsList.add(i.value);
}
// 默认倍速
speedsList = List<double>.from(videoStorage
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
//playSpeedSystem
final List<double> playSpeedSystem =
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
// for (final PlaySpeed i in PlaySpeed.values) {
speedsList.addAll(playSpeedSystem);
// }
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) {
@ -535,8 +542,10 @@ class PlPlayerController {
if (event) {
playerStatus.status.value = PlayerStatus.playing;
} else {
// playerStatus.status.value = PlayerStatus.paused;
playerStatus.status.value = PlayerStatus.paused;
}
videoPlayerServiceHandler.onStatusChange(
playerStatus.status.value, isBuffering.value);
/// 触发回调事件
for (var element in _statusListeners) {
@ -675,18 +684,6 @@ class PlPlayerController {
_playbackSpeed.value = speed;
}
/// 设置倍速
// Future<void> togglePlaybackSpeed() async {
// List<double> allowedSpeeds =
// PlaySpeed.values.map<double>((e) => e.value).toList();
// int index = allowedSpeeds.indexOf(_playbackSpeed.value);
// if (index < allowedSpeeds.length - 1) {
// setPlaybackSpeed(allowedSpeeds[index + 1]);
// } else {
// setPlaybackSpeed(allowedSpeeds[0]);
// }
// }
/// 播放视频
/// TODO _duration.value丢失
Future<void> play(

View File

@ -1,39 +1,15 @@
enum PlaySpeed {
pointTwoFive,
pointFive,
pointSevenFive,
List<double> generatePlaySpeedList() {
List<double> playSpeed = [];
double startSpeed = 0.25;
double endSpeed = 2.0;
double increment = 0.25;
one,
onePointTwoFive,
onePointFive,
onePointSevenFive,
for (double speed = startSpeed; speed <= endSpeed; speed += increment) {
playSpeed.add(speed);
}
two,
return playSpeed;
}
extension PlaySpeedExtension on PlaySpeed {
static final List<String> _descList = [
'0.25',
'0.5',
'0.75',
'正常',
'1.25',
'1.5',
'1.75',
'2.0',
];
String get description => _descList[index];
static final List<double> _valueList = [
0.25,
0.5,
0.75,
1.0,
1.25,
1.5,
1.75,
2.0,
];
double get value => _valueList[index];
double get defaultValue => _valueList[3];
}
// 导出 playSpeed 列表
List<double> playSpeed = generatePlaySpeedList();

View File

@ -26,6 +26,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
static final List<MediaItem> _item = [];
Box setting = GStrorage.setting;
bool enableBackgroundPlay = false;
PlPlayerController player = PlPlayerController.getInstance();
VideoPlayerServiceHandler() {
revalidateSetting();
@ -38,12 +39,12 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
@override
Future<void> play() async {
PlPlayerController.getInstance().play();
player.play();
}
@override
Future<void> pause() async {
PlPlayerController.getInstance().pause();
player.pause();
}
@override
@ -51,7 +52,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
playbackState.add(playbackState.value.copyWith(
updatePosition: position,
));
await PlPlayerController.getInstance().seekTo(position);
await player.seekTo(position);
}
Future<void> setMediaItem(MediaItem newMediaItem) async {

View File

@ -20,7 +20,7 @@ class AudioSessionHandler {
session.interruptionEventStream.listen((event) {
final player = PlPlayerController.getInstance();
if (event.begin) {
if (player.playerStatus != PlayerStatus.playing) return;
if (!player.playerStatus.playing) return;
switch (event.type) {
case AudioInterruptionType.duck:
player.setVolume(player.volume.value * 0.5);
@ -52,7 +52,7 @@ class AudioSessionHandler {
// 耳机拔出暂停
session.becomingNoisyEventStream.listen((_) {
final player = PlPlayerController.getInstance();
if (player.playerStatus == PlayerStatus.playing) {
if (player.playerStatus.playing) {
player.pause();
}
});

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

@ -99,10 +99,8 @@ class CacheManage {
try {
// 清除缓存 图片缓存
await clearLibraryCache();
Timer(const Duration(milliseconds: 500), () {
SmartDialog.dismiss().then((res) {
SmartDialog.showToast('清除完成');
});
SmartDialog.dismiss().then((res) {
SmartDialog.showToast('清除完成');
});
} catch (err) {
SmartDialog.dismiss();

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

@ -130,7 +130,9 @@ class SettingBoxKey {
enableWordRe = 'enableWordRe',
enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi';
enableAi = 'enableAi',
defaultHomePage = 'defaultHomePage',
enableRelatedVideo = 'enableRelatedVideo';
/// 外观
static const String themeMode = 'themeMode',
@ -180,6 +182,8 @@ class VideoBoxKey {
videoSpeed = 'videoSpeed',
// 播放顺序
playRepeat = 'playRepeat',
// 系统预设倍速
playSpeedSystem = 'playSpeedSystem',
// 默认倍速
playSpeedDefault = 'playSpeedDefault',
// 默认长按倍速

View File

@ -14,19 +14,23 @@ class UrlUtils {
dio.options.validateStatus = (status) {
return status == 200 || status == 301 || status == 302;
};
final response = await dio.get(url);
if (response.statusCode == 302) {
redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
}
} else {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
try {
final response = await dio.get(url);
if (response.statusCode == 302) {
redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
}
} else {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
return url;
}
return redirectUrl;
} catch (err) {
return url;
}
return redirectUrl;
}
// 匹配url路由跳转

View File

@ -50,6 +50,9 @@ class Utils {
return time;
}
if (time < 3600) {
if (time == 0) {
return time;
}
final int minute = time ~/ 60;
final double res = time / 60;
if (minute != res) {
@ -87,6 +90,9 @@ class Utils {
// 时间显示刚刚x分钟前
static String dateFormat(timeStamp, {formatType = 'list'}) {
if (timeStamp == 0 || timeStamp == null || timeStamp == '') {
return '';
}
// 当前时间
int time = (DateTime.now().millisecondsSinceEpoch / 1000).round();
// 对比
@ -103,6 +109,7 @@ class Utils {
toInt: false,
formatType: formatType);
}
print('distance: $distance');
if (distance <= 60) {
return '刚刚';
} else if (distance <= 3600) {
@ -236,6 +243,10 @@ class Utils {
SmartDialog.dismiss();
var currentInfo = await PackageInfo.fromPlatform();
var result = await Request().get(Api.latestApp, extra: {'ua': 'mob'});
if (result.data == null || result.data.isEmpty) {
SmartDialog.showToast('获取远程版本失败,请检查网络');
return false;
}
LatestDataModel data = LatestDataModel.fromJson(result.data);
bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!);
if (isUpdate) {
@ -344,9 +355,8 @@ class Utils {
}
static List<int> generateRandomBytes(int minLength, int maxLength) {
return List<int>.generate(
random.nextInt(maxLength-minLength+1), (_) => random.nextInt(0x60) + 0x20
);
return List<int>.generate(random.nextInt(maxLength - minLength + 1),
(_) => random.nextInt(0x60) + 0x20);
}
static String base64EncodeRandomString(int minLength, int maxLength) {

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"

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
# 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.
version: 1.0.19+1019
version: 1.0.21+1021
environment:
sdk: ">=2.19.6 <3.0.0"