Compare commits

..

31 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
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
22 changed files with 782 additions and 431 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/.*" /> android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" /> 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> </intent-filter>
</activity> </activity>

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

@ -3,11 +3,13 @@ class FollowUpModel {
this.liveUsers, this.liveUsers,
this.upList, this.upList,
this.liveList, this.liveList,
this.myInfo,
}); });
LiveUsers? liveUsers; LiveUsers? liveUsers;
List<UpItem>? upList; List<UpItem>? upList;
List<LiveUserItem>? liveList; List<LiveUserItem>? liveList;
MyInfo? myInfo;
FollowUpModel.fromJson(Map<String, dynamic> json) { FollowUpModel.fromJson(Map<String, dynamic> json) {
liveUsers = json['live_users'] != null liveUsers = json['live_users'] != null
@ -21,6 +23,7 @@ class FollowUpModel {
upList = json['up_list'] != null upList = json['up_list'] != null
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList() ? 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']; 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

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

View File

@ -258,6 +258,10 @@ class DynamicsController extends GetxController {
if (upData.value.upList!.isEmpty) { if (upData.value.upList!.isEmpty) {
mid.value = -1; mid.value = -1;
} }
upData.value.upList!.insertAll(0, [
UpItem(face: '', uname: '全部动态', mid: -1),
UpItem(face: userInfo.face, uname: '', mid: userInfo.mid),
]);
} }
return res; return res;
} }

View File

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

View File

@ -1,16 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart'; import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/dynamics/controller.dart'; import 'package:pilipala/pages/dynamics/controller.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class UpPanel extends StatefulWidget { class UpPanel extends StatefulWidget {
final FollowUpModel? upData; final FollowUpModel upData;
const UpPanel(this.upData, {Key? key}) : super(key: key); const UpPanel(this.upData, {Key? key}) : super(key: key);
@override @override
@ -24,33 +22,17 @@ class _UpPanelState extends State<UpPanel> {
List<UpItem> upList = []; List<UpItem> upList = [];
List<LiveUserItem> liveList = []; List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0); static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
Box userInfoCache = GStrorage.userInfo; late MyInfo userInfo;
var userInfo;
@override void listFormat() {
void initState() { userInfo = widget.upData.myInfo!;
super.initState(); upList = widget.upData.upList!;
upList = widget.upData!.upList!; liveList = widget.upData.liveList!;
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,
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
listFormat();
return SliverPersistentHeader( return SliverPersistentHeader(
floating: true, floating: true,
pinned: false, pinned: false,

View File

@ -12,6 +12,7 @@ import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart'; import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[
@ -19,44 +20,7 @@ class MainController extends GetxController {
const DynamicsPage(), const DynamicsPage(),
const MediaPage(), const MediaPage(),
]; ];
RxList navigationBars = [ RxList navigationBars = defaultNavigationBars.obs;
{
'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;
final StreamController<bool> bottomBarStream = final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -75,6 +39,10 @@ class MainController extends GetxController {
Utils.checkUpdata(); Utils.checkUpdata();
} }
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
int defaultHomePage =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
selectedIndex = defaultNavigationBars
.indexWhere((item) => item['id'] == defaultHomePage);
var userInfo = userInfoCache.get('userInfoCache'); var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(

View File

@ -135,115 +135,103 @@ class _ImagePreviewState extends State<ImagePreview>
), ),
body: Stack( body: Stack(
children: [ children: [
DismissiblePage( GestureDetector(
backgroundColor: Colors.transparent, onLongPress: () => onOpenMenu(),
onDismissed: () { child: ExtendedImageGesturePageView.builder(
Navigator.of(context).pop(); controller: ExtendedPageController(
}, initialPage: _previewController.initialPage.value,
// Note that scrollable widget inside DismissiblePage might limit the functionality pageSpacing: 0,
// 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,
);
},
);
},
), ),
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( Positioned(
@ -251,33 +239,49 @@ class _ImagePreviewState extends State<ImagePreview>
right: 0, right: 0,
bottom: 0, bottom: 0,
child: Container( child: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 30), left: 20,
decoration: const BoxDecoration( right: 20,
gradient: LinearGradient( bottom: MediaQuery.of(context).padding.bottom + 30),
begin: Alignment.topCenter, decoration: const BoxDecoration(
end: Alignment.bottomCenter, gradient: LinearGradient(
colors: <Color>[ begin: Alignment.topCenter,
Colors.transparent, end: Alignment.bottomCenter,
Colors.black87, 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/login.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../models/common/dynamic_badge_mode.dart'; import '../../models/common/dynamic_badge_mode.dart';
import '../../models/common/nav_bar_config.dart';
import '../main/index.dart'; import '../main/index.dart';
import 'widgets/select_dialog.dart'; import 'widgets/select_dialog.dart';
@ -23,6 +24,7 @@ class SettingController extends GetxController {
Rx<ThemeType> themeType = ThemeType.system.obs; Rx<ThemeType> themeType = ThemeType.system.obs;
var userInfo; var userInfo;
Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs; Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
RxInt defaultHomePage = 0.obs;
@override @override
void onInit() { void onInit() {
@ -40,6 +42,8 @@ class SettingController extends GetxController {
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode, SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)]; defaultValue: DynamicBadgeMode.number.code)];
defaultHomePage.value =
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0);
} }
loginOut() async { loginOut() async {
@ -110,4 +114,24 @@ class SettingController extends GetxController {
SmartDialog.showToast('设置成功'); 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)) .where((i) => tabbarSort.contains((i['type'] as TabType).id))
.map<String>((i) => (i['type'] as TabType).id) .map<String>((i) => (i['type'] as TabType).id)
.toList(); .toList();
if (sortedTabbar.isEmpty) {
SmartDialog.showToast('请至少设置一项!');
return;
}
settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar); settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar);
SmartDialog.showToast('保存成功,下次启动时生效'); SmartDialog.showToast('保存成功,下次启动时生效');
} }

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import 'dart:ui';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -57,9 +58,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late bool autoExitFullcreen; late bool autoExitFullcreen;
late bool autoPlayEnable; late bool autoPlayEnable;
late bool autoPiP; late bool autoPiP;
final Floating floating = Floating(); late Floating floating;
// 生命周期监听
late final AppLifecycleListener _lifecycleListener;
bool isShowing = true; bool isShowing = true;
@override @override
@ -92,8 +91,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoSourceInit(); videoSourceInit();
appbarStreamListen(); appbarStreamListen();
lifecycleListener();
fullScreenStatusListener(); fullScreenStatusListener();
if (Platform.isAndroid) {
floating = videoDetailController.floating!;
autoEnterPip();
}
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
@ -151,6 +153,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
} catch (_) {} } catch (_) {}
} }
if (Platform.isAndroid) {
floating.toggleAutoPip(
autoEnter: status == PlayerStatus.playing && autoPiP);
}
} }
// 继续播放或重新播放 // 继续播放或重新播放
@ -169,27 +175,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoDetailController.isShowCover.value = false; 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() { void fullScreenStatusListener() {
plPlayerController?.isFullScreen.listen((bool isFullScreen) { plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) { if (isFullScreen) {
@ -209,8 +194,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoDetailController.floating!.dispose(); videoDetailController.floating!.dispose();
} }
videoPlayerServiceHandler.onVideoDetailDispose(); videoPlayerServiceHandler.onVideoDetailDispose();
floating.dispose(); if (Platform.isAndroid) {
_lifecycleListener.dispose(); floating.toggleAutoPip(autoEnter: false);
floating.dispose();
}
super.dispose(); super.dispose();
} }
@ -228,7 +215,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
} }
print('🐶🐶');
setState(() => isShowing = false); setState(() => isShowing = false);
super.didPushNext(); super.didPushNext();
} }
@ -264,29 +250,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.subscribe(this, ModalRoute.of(context)! as PageRoute); .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() { void autoEnterPip() {
final String routePath = Get.currentRoute; final String routePath = Get.currentRoute;
final bool isPortrait = if (autoPiP && routePath.startsWith('/video')) {
MediaQuery.of(context).orientation == Orientation.portrait; floating.toggleAutoPip(autoEnter: autoPiP);
/// TODO 横屏全屏状态下误触pip
if (autoPiP && routePath.startsWith('/video') && isPortrait) {
floating.enable(
aspectRatio: Rational(
videoDetailController.data.dash!.video!.first.width!,
videoDetailController.data.dash!.video!.first.height!,
));
} }
} }
@ -406,7 +373,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
), ),
); );
} else { } else {
return const SizedBox(); return buildCustomAppBar();
} }
}, },
), ),
@ -449,33 +416,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: AppBar( child: buildCustomAppBar(),
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)
],
),
), ),
Positioned( Positioned(
right: 12, right: 12,
@ -657,4 +598,48 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return childWhenDisabled; 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

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

View File

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

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

View File

@ -130,7 +130,8 @@ class SettingBoxKey {
enableWordRe = 'enableWordRe', enableWordRe = 'enableWordRe',
enableSearchWord = 'enableSearchWord', enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy', enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi'; enableAi = 'enableAi',
defaultHomePage = 'defaultHomePage';
/// 外观 /// 外观
static const String themeMode = 'themeMode', static const String themeMode = 'themeMode',

View File

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