Merge branch 'main' into feature-danmaku

This commit is contained in:
guozhigq
2023-08-27 18:40:39 +08:00
32 changed files with 876 additions and 446 deletions

View File

@ -20,7 +20,22 @@
"android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<application
<queries>
<!-- If your app checks for http support -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
<application
android:label="PiliPala"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
@ -48,6 +63,164 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bilibili" android:host="forward" />
<data android:scheme="bilibili" android:host="comment"
android:pathPattern="/detail/.*/.*/.*" />
<data android:scheme="bilibili" android:host="uper" />
<data android:scheme="bilibili" android:host="article"
android:pathPattern="/readlist" />
<data android:scheme="bilibili" android:host="advertise" android:path="/home" />
<data android:scheme="bilibili" android:host="clip" />
<data android:scheme="bilibili" android:host="search" />
<data android:scheme="bilibili" android:host="stardust-search" />
<data android:scheme="bilibili" android:host="music" />
<data android:scheme="bilibili" android:host="bangumi"
android:pathPattern="/season.*" />
<data android:scheme="bilibili" android:host="bangumi" android:pathPattern="/.*" />
<data android:scheme="bilibili" android:host="pictureshow"
android:pathPrefix="/creative_center" />
<data android:scheme="bilibili" android:host="cliparea" />
<data android:scheme="bilibili" android:host="im" />
<data android:scheme="bilibili" android:host="im" android:path="/notifications" />
<data android:scheme="bilibili" android:host="following" />
<data android:scheme="bilibili" android:host="following"
android:pathPattern="/detail/.*" />
<data android:scheme="bilibili" android:host="following"
android:path="/publishInfo/" />
<data android:scheme="bilibili" android:host="laser" android:pathPattern="/.*" />
<data android:scheme="bilibili" android:host="livearea" />
<data android:scheme="bilibili" android:host="live" />
<data android:scheme="bilibili" android:host="catalog" />
<data android:scheme="bilibili" android:host="browser" />
<data android:scheme="bilibili" android:host="user_center" />
<data android:scheme="bilibili" android:host="login" />
<data android:scheme="bilibili" android:host="space" />
<data android:scheme="bilibili" android:host="author" />
<data android:scheme="bilibili" android:host="tag" />
<data android:scheme="bilibili" android:host="rank" />
<data android:scheme="bilibili" android:host="external" />
<data android:scheme="bilibili" android:host="blank" />
<data android:scheme="bilibili" android:host="home" />
<data android:scheme="bilibili" android:host="root" />
<data android:scheme="bilibili" android:host="video" />
<data android:scheme="bilibili" android:host="story" />
<data android:scheme="bilibili" android:host="podcast" />
<data android:scheme="bilibili" android:host="search" />
<data android:scheme="bilibili" android:host="main" android:path="/favorite" />
<data android:scheme="bilibili" android:host="pgc" android:path="/theater/match" />
<data android:scheme="bilibili" android:host="pgc" android:path="/theater/square" />
<data android:scheme="bilibili" android:host="m.bilibili.com"
android:path="/topic-detail" />
<data android:scheme="bilibili" android:host="article" />
<data android:scheme="bilibili" android:host="pegasus"
android:pathPattern="/channel/v2/.*" />
<data android:scheme="bilibili" android:host="feed" android:pathPattern="/channel" />
<data android:scheme="bilibili" android:host="vip" />
<data android:scheme="bilibili" android:host="user_center" android:path="/vip" />
<data android:scheme="bilibili" android:host="history" />
<data android:scheme="bilibili" android:host="charge" android:path="/rank" />
<data android:scheme="bilibili" android:host="assistant" />
<data android:scheme="bilibili" android:host="assistant" />
<data android:scheme="bilibili" android:host="feedback" />
<data android:scheme="bilibili" android:host="auth" android:path="/launch" />
<data android:scheme="http" android:host="live.bilibili.com"
android:pathPattern="/live/.*" />
<data android:scheme="https" android:host="live.bilibili.com"
android:pathPattern="/live/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/story/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/story/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="http" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" />
<data android:scheme="https" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/play/ss.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" />
<data android:scheme="http" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="http" android:host="bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@ -56,7 +229,6 @@
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

View File

@ -1,4 +1,6 @@
PODS:
- appscheme (1.0.4):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
@ -45,6 +47,7 @@ PODS:
- Flutter
DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
@ -71,6 +74,8 @@ SPEC REPOS:
- ReachabilitySwift
EXTERNAL SOURCES:
appscheme:
:path: ".symlinks/plugins/appscheme/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
@ -111,6 +116,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854

View File

@ -1,62 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PiliPala</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>pilipala</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key>
<string>App需要您的同意,才能访问媒体资料库</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
</dict>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PiliPala</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>PiliPala</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key>
<string>App需要您的同意,才能访问媒体资料库</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
<!-- Add Scheme related information -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>http</string>
<string>https</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>m.bilibili.com</string>
<string>bilibili.com</string>
<string>www.bilibili.com</string>
<string>bangumi.bilibili.com</string>
<string>bilibili.cn</string>
<string>www.bilibili.cn</string>
<string>bangumi.bilibili.cn</string>
<string>bilibili.tv</string>
<string>www.bilibili.tv</string>
<string>bangumi.bilibili.tv</string>
<string>miniapp.bilibili.com</string>
<string>live.bilibili.com</string>
</array>
</dict>
</array>
</dict>
<!-- 当其他应用程序或系统通过 bilibili -->
<dict>
<key>CFBundleURLName</key>
<string>bilibili</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bilibili</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -45,11 +45,6 @@ class VideoCardVSkeleton extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 12),
color: Theme.of(context).colorScheme.onInverseSurface,
),
Container(
width: 80,
height: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
),
),

View File

@ -15,12 +15,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
final dynamic videoItem;
final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
const VideoCardV({
Key? key,
required this.videoItem,
required this.crossAxisCount,
this.longPress,
this.longPressEnd,
}) : super(key: key);
@ -77,7 +79,7 @@ class VideoCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 1,
elevation: crossAxisCount == 1 ? 0 : 1,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: GestureDetector(
@ -100,17 +102,27 @@ class VideoCardV extends StatelessWidget {
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
if (crossAxisCount == 1)
PBadge(
bottom: 10,
right: 10,
text: videoItem.duration,
)
],
);
}),
),
VideoContent(videoItem: videoItem)
VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount)
],
),
),
@ -121,22 +133,47 @@ class VideoCardV extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
const VideoContent({Key? key, required this.videoItem}) : super(key: key);
final int crossAxisCount;
const VideoContent(
{Key? key, required this.videoItem, required this.crossAxisCount})
: super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Padding(
padding: const EdgeInsets.fromLTRB(9, 8, 9, 4),
padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
: const EdgeInsets.fromLTRB(9, 8, 9, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
Row(
children: [
Expanded(
child: Text(
videoItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (videoItem.goto == 'av' && crossAxisCount == 1) ...[
const SizedBox(width: 10),
WatchLater(
size: 32,
iconSize: 18,
callFn: () async {
int aid = videoItem.param;
var res =
await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
),
],
],
),
if (crossAxisCount == 1) const SizedBox(height: 6),
Row(
children: [
if (videoItem.goto == 'bangumi') ...[
@ -167,6 +204,7 @@ class VideoContent extends StatelessWidget {
)
],
Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Text(
videoItem.owner.name,
maxLines: 1,
@ -177,95 +215,33 @@ class VideoContent extends StatelessWidget {
),
),
),
if (videoItem.goto == 'av')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
int aid = videoItem.param;
var res = await UserHttp.toViewLater(
bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
if (crossAxisCount == 1) ...[
Text(
'',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
VideoStat(
videoItem: videoItem,
)
],
const Spacer(),
if (videoItem.goto == 'av' && crossAxisCount != 1)
WatchLater(
size: 24,
iconSize: 14,
callFn: () async {
int aid = videoItem.param;
var res =
await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
),
],
),
// Row(
// children: [
// const SizedBox(width: 1),
// StatView(
// theme: 'gray',
// view: videoItem.stat.view,
// ),
// const SizedBox(width: 10),
// StatDanMu(
// theme: 'gray',
// danmu: videoItem.stat.danmaku,
// ),
// const Spacer(),
// SizedBox(
// width: 24,
// height: 24,
// child: PopupMenuButton<String>(
// padding: EdgeInsets.zero,
// tooltip: '稍后再看',
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// position: PopupMenuPosition.under,
// // constraints: const BoxConstraints(maxHeight: 35),
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) =>
// <PopupMenuEntry<String>>[
// PopupMenuItem<String>(
// onTap: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// value: 'pause',
// height: 35,
// child: const Row(
// children: [
// Icon(Icons.watch_later_outlined, size: 16),
// SizedBox(width: 6),
// Text('稍后再看', style: TextStyle(fontSize: 13))
// ],
// ),
// ),
// ],
// ),
// ),
// ],
// ),
],
),
),
@ -274,53 +250,77 @@ class VideoContent extends StatelessWidget {
}
class VideoStat extends StatelessWidget {
final int? view;
final int? danmaku;
final int? duration;
final dynamic videoItem;
const VideoStat(
{Key? key,
required this.view,
required this.danmaku,
required this.duration})
: super(key: key);
const VideoStat({
Key? key,
required this.videoItem,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 48,
padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
StatView(
theme: 'white',
view: view,
),
const SizedBox(width: 6),
StatDanMu(
theme: 'white',
danmu: danmaku,
),
],
return Row(
children: [
Text(
'${videoItem.stat.view}次观看',
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Text(
'${videoItem.stat.danmu}条弹幕',
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
);
}
}
class WatchLater extends StatelessWidget {
final double? size;
final double? iconSize;
final Function? callFn;
const WatchLater({
Key? key,
required this.size,
required this.iconSize,
this.callFn,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: iconSize,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () => callFn!(),
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
Text(
Utils.timeFormat(duration!),
style: const TextStyle(fontSize: 11, color: Colors.white),
)
],
),
);

View File

@ -13,6 +13,7 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
@ -25,9 +26,6 @@ void main() async {
.then((_) async {
await GStrorage.init();
runApp(const MyApp());
await Request.setCookie();
await Data.init();
await GStrorage.lazyInit();
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
@ -35,6 +33,10 @@ void main() async {
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
await Request.setCookie();
Data.init();
GStrorage.lazyInit();
PiliSchame.init();
});
}

View File

@ -1,17 +1,27 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/storage.dart';
class LiveController extends GetxController {
final ScrollController scrollController = ScrollController();
int count = 12;
int _currentPage = 1;
int crossAxisCount = 2;
RxInt crossAxisCount = 2.obs;
RxList<LiveItemModel> liveList = [LiveItemModel()].obs;
bool isLoadingMore = false;
bool flag = false;
OverlayEntry? popupDialog;
Box setting = GStrorage.setting;
@override
void onInit() {
super.onInit();
crossAxisCount.value =
setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2;
}
// 获取推荐
Future queryLiveList(type) async {

View File

@ -129,14 +129,15 @@ class _LivePageState extends State<LivePage> {
}
Widget contentGrid(ctr, liveList) {
double maxWidth = Get.size.width;
int baseWidth = 500;
int step = 300;
int crossAxisCount =
maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
if (maxWidth < 300) {
crossAxisCount = 1;
}
// double maxWidth = Get.size.width;
// int baseWidth = 500;
// int step = 300;
// int crossAxisCount =
// maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
// if (maxWidth < 300) {
// crossAxisCount = 1;
// }
int crossAxisCount = ctr.crossAxisCount.value;
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
@ -147,13 +148,15 @@ class _LivePageState extends State<LivePage> {
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / crossAxisCount / StyleString.aspectRatio +
68 * MediaQuery.of(context).textScaleFactor,
(crossAxisCount == 1 ? 48 : 68) *
MediaQuery.of(context).textScaleFactor,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return liveList!.isNotEmpty
? LiveCardV(
liveItem: liveList[index],
crossAxisCount: crossAxisCount,
longPress: () {
_liveController.popupDialog =
_createPopupDialog(liveList[index]);

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -9,12 +8,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class LiveCardV extends StatelessWidget {
final LiveItemModel liveItem;
final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
const LiveCardV({
Key? key,
required this.liveItem,
required this.crossAxisCount,
this.longPress,
this.longPressEnd,
}) : super(key: key);
@ -23,7 +24,7 @@ class LiveCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card(
elevation: 1,
elevation: crossAxisCount == 1 ? 0 : 1,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: GestureDetector(
@ -45,12 +46,7 @@ class LiveCardV extends StatelessWidget {
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
borderRadius: const BorderRadius.all(StyleString.imgRadius),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
@ -66,24 +62,25 @@ class LiveCardV extends StatelessWidget {
height: maxHeight,
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: VideoStat(
liveItem: liveItem,
if (crossAxisCount != 1)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: VideoStat(
liveItem: liveItem,
),
),
),
),
],
);
}),
),
),
LiveContent(liveItem: liveItem)
LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount)
],
),
),
@ -94,13 +91,18 @@ class LiveCardV extends StatelessWidget {
class LiveContent extends StatelessWidget {
final dynamic liveItem;
const LiveContent({Key? key, required this.liveItem}) : super(key: key);
final int crossAxisCount;
const LiveContent(
{Key? key, required this.liveItem, required this.crossAxisCount})
: super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(9, 9, 9, 8),
padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
: const EdgeInsets.fromLTRB(9, 8, 9, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -112,29 +114,40 @@ class LiveContent extends StatelessWidget {
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
maxLines: crossAxisCount == 1 ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
if (crossAxisCount == 1) const SizedBox(height: 4),
Row(
children: [
const PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
Text(
liveItem.uname,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: Text(
liveItem.uname,
textAlign: TextAlign.start,
if (crossAxisCount == 1) ...[
Text(
'${liveItem!.areaName!}',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
Text(
'${liveItem!.watchedShow!['text_small']}人观看',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
]
],
),
],

View File

@ -29,10 +29,10 @@ class LiveRoomController extends GetxController {
if (Get.arguments != null) {
liveItem = Get.arguments['liveItem'];
heroTag = Get.arguments['heroTag'] ?? '';
if (liveItem.pic != null && liveItem.pic != '') {
if (liveItem != null && liveItem.pic != null && liveItem.pic != '') {
cover = liveItem.pic;
}
if (liveItem.cover != null && liveItem.cover != '') {
if (liveItem != null && liveItem.cover != null && liveItem.cover != '') {
cover = liveItem.cover;
}
}

View File

@ -48,32 +48,35 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController.liveItem.watchedShow != null)
Text(
_liveRoomController.liveItem.watchedShow['text_large'] ??
'',
style: const TextStyle(fontSize: 12)),
],
),
],
),
title: _liveRoomController.liveItem != null
? Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController.liveItem.watchedShow != null)
Text(
_liveRoomController
.liveItem.watchedShow['text_large'] ??
'',
style: const TextStyle(fontSize: 12)),
],
),
],
)
: const SizedBox(),
// actions: [
// SizedBox(
// height: 34,
@ -94,21 +97,22 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
? PLVideoPlayer(controller: plPlayerController!)
: const SizedBox(),
),
if (_liveRoomController.liveItem.cover != null)
Visibility(
visible: isShowCover,
child: Positioned(
top: 0,
left: 0,
right: 0,
child: NetworkImgLayer(
type: 'emote',
src: _liveRoomController.liveItem.cover,
width: Get.size.width,
height: videoHeight,
),
),
),
// if (_liveRoomController.liveItem != null &&
// _liveRoomController.liveItem.cover != null)
// Visibility(
// visible: isShowCover,
// child: Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: NetworkImgLayer(
// type: 'emote',
// src: _liveRoomController.liveItem.cover,
// width: Get.size.width,
// height: videoHeight,
// ),
// ),
// ),
],
),
),

View File

@ -12,10 +12,14 @@ class RcmdController extends GetxController {
bool isLoadingMore = true;
OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
Box setting = GStrorage.setting;
RxInt crossAxisCount = 2.obs;
@override
void onInit() {
super.onInit();
crossAxisCount.value =
setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2;
if (recVideo.get('cacheList') != null &&
recVideo.get('cacheList').isNotEmpty) {
List<RecVideoItemAppModel> list = [];

View File

@ -142,31 +142,34 @@ class _RcmdPageState extends State<RcmdPage>
}
Widget contentGrid(ctr, videoList) {
double maxWidth = Get.size.width;
int baseWidth = 500;
int step = 300;
int crossAxisCount =
maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
if (maxWidth < 300) {
crossAxisCount = 1;
}
// double maxWidth = Get.size.width;
// int baseWidth = 500;
// int step = 300;
// int crossAxisCount =
// maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
// if (maxWidth < 300) {
// crossAxisCount = 1;
// }
int crossAxisCount = ctr.crossAxisCount.value;
double mainAxisExtent =
(Get.size.width / crossAxisCount / StyleString.aspectRatio) +
68 * MediaQuery.of(context).textScaleFactor;
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace + 4,
mainAxisSpacing: StyleString.safeSpace,
// 列间距
crossAxisSpacing: StyleString.cardSpace + 4,
crossAxisSpacing: StyleString.safeSpace,
// 列数
crossAxisCount: crossAxisCount,
mainAxisExtent:
(Get.size.width / crossAxisCount / StyleString.aspectRatio) +
68 * MediaQuery.of(context).textScaleFactor,
mainAxisExtent: mainAxisExtent,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return videoList!.isNotEmpty
? VideoCardV(
videoItem: videoList[index],
crossAxisCount: crossAxisCount,
longPress: () {
_rcmdController.popupDialog =
_createPopupDialog(videoList[index]);

View File

@ -21,6 +21,8 @@ class SSearchController extends GetxController {
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
String hintText = '搜索';
RxString defaultSearch = '输入关键词搜索'.obs;
Box setting = GStrorage.setting;
bool enableHotKey = true;
@override
void onInit() {
@ -38,6 +40,7 @@ class SSearchController extends GetxController {
}
historyCacheList = histiryWord.get('cacheList') ?? [];
historyList.value = historyCacheList;
enableHotKey = setting.get(SettingBoxKey.enableHotKey, defaultValue: true);
}
void onChange(value) {

View File

@ -146,7 +146,9 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
// 搜索建议
_searchSuggest(),
// 热搜
hotSearch(_searchController),
Visibility(
visible: _searchController.enableHotKey,
child: hotSearch(_searchController)),
// 搜索历史
_history()
],

View File

@ -56,14 +56,20 @@ class SearchPanelController extends GetxController {
// 匹配输入内容如果是AV、BV号且有结果 直接跳转详情页
Map matchRes = IdUtils.matchAvorBv(input: keyword);
List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty && searchType == SearchType.video) {
String bvid = resultList.first.bvid;
int aid = resultList.first.aid;
String bvid = resultList.first.bvid;
// keyword 可能输入纯数字
int aid = resultList.first.aid;
if (matchKeys.isNotEmpty && searchType == SearchType.video ||
aid.toString() == keyword) {
String heroTag = Utils.makeHeroTag(bvid);
int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (matchKeys.first == 'BV' && matchRes[matchKeys.first] == bvid ||
matchKeys.first == 'AV' && matchRes[matchKeys.first] == aid) {
if (matchKeys.isNotEmpty &&
matchKeys.first == 'BV' &&
matchRes[matchKeys.first] == bvid ||
matchKeys.isNotEmpty &&
matchKeys.first == 'AV' &&
matchRes[matchKeys.first] == aid ||
aid.toString() == keyword) {
Get.toNamed(
'/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': resultList.first, 'heroTag': heroTag},

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart';
@ -47,6 +48,13 @@ class _ExtraSettingState extends State<ExtraSetting> {
),
body: ListView(
children: [
SetSwitchItem(
title: '大家都在搜',
subTitle: '是否展示「大家都在搜」',
setKey: SettingBoxKey.enableHotKey,
defaultVal: true,
callFn: (val) => {SmartDialog.showToast('下次启动时生效')},
),
ListTile(
dense: false,
title: Text('评论展示', style: titleStyle),

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/theme_type.dart';
@ -75,6 +76,13 @@ class _StyleSettingState extends State<StyleSetting> {
setKey: SettingBoxKey.iosTransition,
defaultVal: false,
),
SetSwitchItem(
title: '首页单列',
subTitle: '每行展示一个内容卡片',
setKey: SettingBoxKey.enableSingleRow,
defaultVal: false,
callFn: (val) => {SmartDialog.showToast('下次启动时生效')},
),
ListTile(
dense: false,
onTap: () {

View File

@ -8,12 +8,14 @@ class SetSwitchItem extends StatefulWidget {
final String? subTitle;
final String? setKey;
final bool? defaultVal;
final Function? callFn;
const SetSwitchItem({
this.title,
this.subTitle,
this.setKey,
this.defaultVal,
this.callFn,
Key? key,
}) : super(key: key);
@ -32,12 +34,15 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false);
}
void switchChange(value) {
void switchChange(value) async {
val = value ?? !val;
Setting.put(widget.setKey, val);
await Setting.put(widget.setKey, val);
if (widget.setKey == SettingBoxKey.autoUpdate && value == true) {
Utils.checkUpdata();
}
if (widget.callFn != null) {
widget.callFn!.call(val);
}
setState(() {});
}

View File

@ -209,27 +209,17 @@ class VideoDetailController extends GetxController
if (result['status']) {
data = result['data'];
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
// firstVideo = data.dash!.video!.first;
// videoUrl = firstVideo.baseUrl!;
// //
// currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
// AudioItem firstAudio =
// data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
// audioUrl = firstAudio.baseUrl ?? '';
List<VideoItem> allVideosList = data.dash!.video!;
try {
// 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code;
//
// 使用预设的画质 当前可用的最高质量
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: currentHighVideoQa);
int resVideoQa = currentHighVideoQa;
if (cacheVideoQa <= currentHighVideoQa) {
// 如果预设的画质低于当前最高
List<int> numbers = data.acceptQuality!
.where((e) => e <= currentHighVideoQa)
.toList();
@ -250,10 +240,16 @@ class VideoDetailController extends GetxController
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
print(currentDecodeFormats.description);
try {
// 当前视频没有对应格式返回第一个
currentDecodeFormats = supportDecodeFormats
.contains(currentDecodeFormats)
bool flag = false;
for (var i in supportDecodeFormats) {
if (i.startsWith(currentDecodeFormats.code)) {
flag = true;
}
}
currentDecodeFormats = flag
? currentDecodeFormats
: VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!;
} catch (e) {
@ -297,13 +293,6 @@ class VideoDetailController extends GetxController
}
defaultST = Duration(milliseconds: data.lastPlayTime!);
await playerInit();
// await playerInit(
// firstVideo,
// audioUrl,
// defaultST: Duration(milliseconds: data.lastPlayTime!),
// duration: data.timeLength ?? 0,
// );
} else {
if (result['code'] == -404) {
isShowCover.value = false;

View File

@ -602,9 +602,26 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
}),
..onTap = () {
String appUrlSchema =
content.jumpUrl[matchStr]['app_url_schema'];
if (appUrlSchema == '') {
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
);
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
},
),
);
spanChilds.add(

View File

@ -149,7 +149,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
key: videoDetailController.scaffoldKey,
// fix 1px black line
// backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
backgroundColor: Colors.black,
body: ExtendedNestedScrollView(
controller: _extendNestCtr,
headerSliverBuilder:
@ -162,11 +162,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled,
expandedHeight: videoHeight,
backgroundColor:
MediaQuery.of(Get.context!).platformBrightness ==
Brightness.dark
? Colors.black
: Theme.of(context).colorScheme.background,
backgroundColor: Colors.black,
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(top: statusBarHeight),

View File

@ -167,50 +167,46 @@ class _HeaderControlState extends State<HeaderControl> {
/// 选择倍速
void showSetSpeedSheet() {
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 450,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Material(
child: ListView(
physics: const NeverScrollableScrollPhysics(),
double currentSpeed = widget.controller!.playbackSpeed;
SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (context) {
return AlertDialog(
title: const Text('播放速度'),
contentPadding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 45,
child: Center(
child: Text('播放速度'),
),
),
for (var i in playSpeed) ...[
ListTile(
onTap: () {
widget.controller!.setPlaybackSpeed(i.value);
Get.back(result: {'playbackSpeed': i.value});
},
dense: true,
contentPadding: const EdgeInsets.only(left: 20, right: 20),
title: Text(i.description),
trailing: i.value == widget.controller!.playbackSpeed
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: null,
),
]
Text('$currentSpeed倍'),
Slider(
min: PlaySpeed.values.first.value,
max: PlaySpeed.values.last.value,
value: currentSpeed,
divisions: PlaySpeed.values.length - 1,
label: '${currentSpeed}x',
onChanged: (double val) =>
{setState(() => currentSpeed = val)},
)
],
);
}),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
),
TextButton(
onPressed: () async {
await SmartDialog.dismiss();
widget.controller!.setPlaybackSpeed(currentSpeed);
},
child: const Text('确定'),
),
],
);
},
);

View File

@ -63,6 +63,13 @@ class WebviewController extends GetxController {
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('bilibili://')) {
if (request.url.startsWith('bilibili://video/')) {
String str = Uri.parse(request.url).pathSegments[0];
Get.offAndToNamed(
'/searchResult',
parameters: {'keyword': str},
);
}
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;

View File

@ -2,11 +2,23 @@ enum PlaySpeed {
pointTwoFive,
pointFive,
pointSevenFive,
one,
onePointTwoFive,
onePointFive,
onePointSevenFive,
two
two,
twoPointTwoFive,
twoPointFive,
twoPointSevenFive,
twhree,
threePointTwoFive,
threePointFive,
threePointSevenFive,
four,
}
extension PlaySpeedExtension on PlaySpeed {
@ -17,8 +29,15 @@ extension PlaySpeedExtension on PlaySpeed {
'正常速度',
'1.25倍',
'1.5倍',
'1.75倍',
'2.0倍',
'2.25倍',
'2.5倍',
'2.75倍',
'3.0倍',
'3.25倍',
'3.5倍',
'3.75倍',
'4.0倍'
];
get description => _descList[index];
@ -30,7 +49,15 @@ extension PlaySpeedExtension on PlaySpeed {
1.25,
1.5,
1.75,
2.0
2.0,
2.25,
2.5,
2.75,
3.0,
3.25,
3.5,
3.75,
4.0,
];
get value => _valueList[index];
get defaultValue => _valueList[3];

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -48,12 +49,17 @@ Future<void> enterFullScreen() async {
//退出全屏显示
Future<void> exitFullScreen() async {
dynamic document;
late SystemUiMode mode = SystemUiMode.edgeToEdge;
try {
if (kIsWeb) {
document.exitFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) {
if (Platform.isAndroid &&
(await DeviceInfoPlugin().androidInfo).version.sdkInt < 29) {
mode = SystemUiMode.manual;
}
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
mode,
overlays: SystemUiOverlay.values,
);
await SystemChrome.setPreferredOrientations([]);

View File

@ -199,22 +199,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
context: Get.context!,
useSafeArea: false,
builder: (context) => Dialog.fullscreen(
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
primary: false,
toolbarHeight: 0,
backgroundColor: Colors.black,
systemOverlayStyle: SystemUiOverlayStyle.light,
backgroundColor: Colors.black,
child: PLVideoPlayer(
controller: _,
headerControl: _.headerControl,
),
body: SafeArea(
bottom: false,
child: PLVideoPlayer(
controller: _,
headerControl: _.headerControl,
),
),
),
),
);
if (result == null) {

View File

@ -30,12 +30,12 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
elevation: 0,
scrolledUnderElevation: 0,
primary: false,
toolbarHeight: 73,
toolbarHeight: 85,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Column(
children: [
const SizedBox(height: 23),
const SizedBox(height: 17),
Obx(
() {
final int value = _.sliderPosition.value.inSeconds;
@ -45,7 +45,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
return Container();
}
return Padding(
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5),
padding: const EdgeInsets.only(left: 7, right: 5, bottom: 6),
child: ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),

106
lib/utils/app_scheme.dart Normal file
View File

@ -0,0 +1,106 @@
import 'package:appscheme/appscheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'id_utils.dart';
import 'utils.dart';
class PiliSchame {
static AppScheme appScheme = AppSchemeImpl.getInstance() as AppScheme;
static void init() async {
///
SchemeEntity? value = await appScheme.getInitScheme();
if (value != null) {
_routePush(value);
}
///
appScheme.getLatestScheme().then((value) {
if (value != null) {}
});
/// 注册从外部打开的Scheme监听信息 #
appScheme.registerSchemeListener().listen((event) {
if (event != null) {
_routePush(event);
}
});
}
/// 路由跳转
static void _routePush(value) async {
String scheme = value.scheme;
String host = value.host;
String path = value.path;
if (scheme == 'bilibili') {
// bilibili://root
if (host == 'root') {
Navigator.popUntil(Get.context!, (route) => route.isFirst);
}
// bilibili://space/{uid}
else if (host == 'space') {
var mid = path.split('/').last;
Get.toNamed(
'/member?mid=$mid',
arguments: {'face': null},
);
}
// bilibili://video/{aid}
else if (host == 'video') {
var pathQuery = path.split('/').last;
int aid = int.parse(pathQuery);
String bvid = IdUtils.av2bv(aid);
int cid = await SearchHttp.ab2c(bvid: bvid);
String heroTag = Utils.makeHeroTag(aid);
Get.toNamed('/video?bvid=$bvid&cid=$cid', arguments: {
'pic': null,
'heroTag': heroTag,
});
}
// bilibili://live/{roomid}
else if (host == 'live') {
var roomId = path.split('/').last;
Get.toNamed('/liveRoom?roomid=$roomId',
arguments: {'liveItem': null, 'heroTag': roomId.toString()});
}
// bilibili://bangumi/season/${ssid}
else if (host == 'bangumi') {
if (path.startsWith('/season')) {
SmartDialog.showLoading(msg: '获取中...');
try {
var seasonId = path.split('/').last;
var result = await SearchHttp.bangumiInfo(
seasonId: int.parse(seasonId), epId: null);
if (result['status']) {
var bangumiDetail = result['data'];
int cid = bangumiDetail.episodes!.first.cid;
String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid);
String heroTag = Utils.makeHeroTag(cid);
var epId = bangumiDetail.episodes!.first.id;
SmartDialog.dismiss().then(
(e) => Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: {
'pic': bangumiDetail.cover,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
},
),
);
}
} catch (e) {
SmartDialog.showToast('失败:${e.toString()}');
}
}
}
}
}
}

View File

@ -105,6 +105,7 @@ class SettingBoxKey {
static const String autoUpdate = 'autoUpdate';
static const String replySortType = 'replySortType';
static const String defaultDynamicType = 'defaultDynamicType';
static const String enableHotKey = 'enableHotKey';
/// 外观
static const String themeMode = 'themeMode';
@ -112,6 +113,7 @@ class SettingBoxKey {
static const String dynamicColor = 'dynamicColor'; // bool
static const String customColor = 'customColor'; // 自定义主题色
static const String iosTransition = 'iosTransition'; // ios路由
static const String enableSingleRow = 'enableSingleRow'; // 首页单列
}
class LocalCacheKey {

View File

@ -25,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
appscheme:
dependency: "direct main"
description:
name: appscheme
sha256: b885b65219f3839ebafc937024a1bc5ce5a75b0e458fd249ef15e80e81235b6f
url: "https://pub.dev"
source: hosted
version: "1.0.8"
archive:
dependency: transitive
description:
@ -713,20 +721,20 @@ packages:
dependency: "direct main"
description:
name: media_kit
sha256: "0a89e7037002a62701ec319c375586849f9ef8e681820e1dd4a4ff7b843f7542"
sha256: "66f04934bcadf592f24d829127471e4dc304de8e9bba5795ade2f3e95552ebfc"
url: "https://pub.dev"
source: hosted
version: "1.1.4+1"
version: "1.1.6"
media_kit_libs_android_video:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "142d389bf3efcf8469594a9c7a06a92fc25843fc6c0c3247f76cdcf70b3b29de"
sha256: "498a5062bc5f000bd23ada3be788ea886ab32c52f7a8252dde1264ca019b819b"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
media_kit_libs_ios_video:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_ios_video
sha256: fed403dc9d54462e51ee80e0cb23c12a53fadea9a8fa18aca2de9054176d1159
@ -734,31 +742,39 @@ packages:
source: hosted
version: "1.1.3"
media_kit_libs_linux:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_linux
sha256: "570bf18ebbd1221caec082657468be05d180510385d3515ec38e0be44fdcc859"
sha256: "3b7c272179639a914dc8a50bf8a3f2df0e9a503bd727c88fab499dbdf6cb1eb8"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
media_kit_libs_macos_video:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_libs_macos_video
sha256: c06e831f3c22a45296d375788d9bc07871b448f8e9ec98d77b11e5e118a83fb2
url: "https://pub.dev"
source: hosted
version: "1.1.3"
media_kit_libs_windows_video:
media_kit_libs_video:
dependency: "direct main"
description:
name: media_kit_libs_windows_video
sha256: f33aabd8414470d99e2c91dd98d605e6a5f1c4b8082dd933c10951bc961b9124
name: media_kit_libs_video
sha256: "48c8ace458f340e6b930c89c48141ea727b80aa0878f7a01904d7d439865f162"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
version: "1.0.0"
media_kit_libs_windows_video:
dependency: transitive
description:
name: media_kit_libs_windows_video
sha256: "923f068344d7d200184e0aaa2597f3de6c05982a3b1f18035d842ab53f2a1350"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
media_kit_native_event_loop:
dependency: "direct main"
dependency: transitive
description:
name: media_kit_native_event_loop
sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4
@ -769,10 +785,10 @@ packages:
dependency: "direct main"
description:
name: media_kit_video
sha256: e7fcbe426d42a78ad6696f8f557adb9cbdc012177829026d04992cc106a1c815
sha256: "809a3797da7d49fad85f139555b352dd615f9d2da6ae9f1745c6978963491bae"
url: "https://pub.dev"
source: hosted
version: "1.1.5"
version: "1.1.7"
meta:
dependency: transitive
description:

View File

@ -79,26 +79,14 @@ dependencies:
flutter_smart_dialog: ^4.9.3+2
# 下滑关闭
dismissible_page: ^1.0.2
# 媒体播放
# flutter_meedu_media_kit:
# path: /Users/rr/Desktop/code/flutter_meedu_media_kit/package
# git:
# url: https://github.com/guozhigq/flutter_meedu_media_kit.git
# ref: feature-custom
# path: package
custom_sliding_segmented_control: ^1.7.5
# 加密
crypto: ^3.0.3
# 视频播放器
media_kit: ^1.1.4 # Primary package.
media_kit_video: ^1.1.5 # For video rendering.
media_kit_native_event_loop: ^1.0.7 # Support for higher number of concurrent instances & better performance.
media_kit_libs_android_video: ^1.3.2 # Android package for video native libraries.
media_kit_libs_ios_video: ^1.1.3 # iOS package for video native libraries.
media_kit_libs_macos_video: ^1.1.3 # macOS package for video native libraries.
media_kit_libs_windows_video: ^1.0.7 # Windows package for video native libraries.
media_kit_libs_linux: ^1.1.1
media_kit: ^1.1.6
media_kit_video: ^1.1.7
media_kit_libs_video: ^1.0.0
# 音量、亮度、屏幕控制
flutter_volume_controller: ^1.2.7
@ -119,6 +107,8 @@ dependencies:
easy_debounce: ^2.0.3
# 高帧率
flutter_displaymode: ^0.6.0
# scheme跳转
appscheme: ^1.0.8
dev_dependencies:
flutter_test: