Merge branch 'main' into fix

This commit is contained in:
guozhigq
2024-12-08 18:45:03 +08:00
27 changed files with 1171 additions and 919 deletions

View File

@ -26,15 +26,50 @@
Xcode 13.4 不支持**auto_orientation**,请注释相关代码 Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```bash ```bash
[] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale [!] Flutter (Channel [user-branch], 3.19.6, on macOS 14.6.1 23G93 darwin-arm64,
zh-Hans-CN) locale zh-Hans-CN)
! Flutter version 3.19.6 on channel [user-branch] at
/Users/rr/Documents/sdk/flutter
Currently on an unknown channel. Run `flutter channel` to switch to an
official channel.
If that doesn't fix the issue, reinstall Flutter by following instructions
at https://flutter.dev/docs/get-started/install.
! Upstream repository unknown source is not a standard remote.
Set environment variable "FLUTTER_GIT_URL" to unknown source to dismiss
this error.
• Framework revision 54e66469a9 (8 months ago), 2024-04-17 13:08:03 -0700
• Engine revision c4cd48e186
• Dart version 3.3.4
• DevTools version 2.31.1
• Pub download mirror https://pub.flutter-io.cn
• Flutter download mirror https://storage.flutter-io.cn
• If those were intentional, you can disregard the above warnings; however
it is recommended to use "git" directly to perform update checks and
upgrades.
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/rr/Library/Android/sdk
• Platform android-34, build-tools 34.0.0
• Java binary at: /Applications/Android
Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
• All Android licenses accepted.
[] Xcode - develop for iOS and macOS (Xcode 15.1) [] Xcode - develop for iOS and macOS (Xcode 15.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 15C65
• CocoaPods version 1.14.3
[] Chrome - develop for the web [] Chrome - develop for the web
[] Android Studio (version 2022.3) • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[] VS Code (version 1.87.2)
[] Connected device (3 available) [] Android Studio (version 2024.2)
[] Network resources • Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
``` ```

View File

@ -778,10 +778,15 @@ class UgcSeasonBuild extends StatelessWidget {
], ],
), ),
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[ if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
const SizedBox(height: 4), const SizedBox(height: 6),
Text( Container(
ugcSeason.intro!, constraints: const BoxConstraints(maxHeight: 100),
style: TextStyle(color: outline, fontSize: 12), child: SingleChildScrollView(
child: Text(
ugcSeason.intro!,
style: TextStyle(color: outline, fontSize: 12),
),
),
), ),
], ],
const SizedBox(height: 4), const SizedBox(height: 4),

View File

@ -1,12 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class GroupPanel extends StatefulWidget { class GroupPanel extends StatefulWidget {
final int? mid; final int? mid;
@ -18,7 +16,6 @@ class GroupPanel extends StatefulWidget {
} }
class _GroupPanelState extends State<GroupPanel> { class _GroupPanelState extends State<GroupPanel> {
final Box<dynamic> localCache = GStorage.localCache;
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late List<MemberTagItemModel> tagsList; late List<MemberTagItemModel> tagsList;
bool showDefault = true; bool showDefault = true;
@ -137,7 +134,7 @@ class _GroupPanelState extends State<GroupPanel> {
left: 20, left: 20,
right: 20, right: 20,
top: 12, top: 12,
bottom: MediaQuery.of(context).padding.bottom + 12, bottom: MediaQuery.paddingOf(context).bottom + 12,
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,

View File

@ -1,13 +1,11 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:cookie_jar/cookie_jar.dart'; import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/utils.dart'; import '../utils/utils.dart';
@ -39,18 +37,7 @@ class Request {
dio.interceptors.add(cookieManager); dio.interceptors.add(cookieManager);
final List<Cookie> cookie = await cookieManager.cookieJar final List<Cookie> cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl)); .loadForRequest(Uri.parse(HttpString.baseUrl));
final UserInfoData? userInfo = userInfoCache.get('userInfoCache'); final userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
final List<Cookie> cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (cookie2.isEmpty) {
try {
await Request().get(HttpString.tUrl);
} catch (e) {
log("setCookie, ${e.toString()}");
}
}
}
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null); setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
String baseUrlType = 'default'; String baseUrlType = 'default';
if (setting.get(SettingBoxKey.enableGATMode, defaultValue: false)) { if (setting.get(SettingBoxKey.enableGATMode, defaultValue: false)) {
@ -69,10 +56,10 @@ class Request {
static Future<String> getCsrf() async { static Future<String> getCsrf() async {
List<Cookie> cookies = await cookieManager.cookieJar List<Cookie> cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl)); .loadForRequest(Uri.parse(HttpString.baseUrl));
String token = ''; String token = cookies
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) { .firstWhere((e) => e.name == 'bili_jct',
token = cookies.firstWhere((e) => e.name == 'bili_jct').value; orElse: () => Cookie('bili_jct', ''))
} .value;
return token; return token;
} }

View File

@ -3,7 +3,7 @@
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/utils/login.dart'; // import 'package:pilipala/utils/login.dart';
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@override @override
@ -19,9 +19,9 @@ class ApiInterceptor extends Interceptor {
void onResponse(Response response, ResponseInterceptorHandler handler) { void onResponse(Response response, ResponseInterceptorHandler handler) {
try { try {
// 在响应之后处理数据 // 在响应之后处理数据
if (response.data is Map && response.data['code'] == -101) { // if (response.data is Map && response.data['code'] == -101) {
LoginUtils.loginOut(); // LoginUtils.loginOut();
} // }
} catch (err) { } catch (err) {
print('ApiInterceptor: $err'); print('ApiInterceptor: $err');
} }
@ -39,6 +39,8 @@ class ApiInterceptor extends Interceptor {
SmartDialog.showToast( SmartDialog.showToast(
await dioError(err), await dioError(err),
displayType: SmartToastType.onlyRefresh, displayType: SmartToastType.onlyRefresh,
displayTime: const Duration(seconds: 1),
debounce: true,
); );
} }
super.onError(err, handler); super.onError(err, handler);
@ -62,7 +64,7 @@ class ApiInterceptor extends Interceptor {
return '发送请求超时,请检查网络设置'; return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown: case DioExceptionType.unknown:
final String res = await checkConnect(); final String res = await checkConnect();
return '$res,网络异常!'; return '$res ${error.error}';
} }
} }

View File

@ -216,6 +216,21 @@ class BuildMainApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Box setting = GStorage.setting;
/// 纯黑模式主题配置
ColorScheme? pureDarkColorScheme;
final bool enablePureBlack =
setting.get(SettingBoxKey.enablePureBlack, defaultValue: false);
if (enablePureBlack) {
pureDarkColorScheme = darkColorScheme.copyWith(
background: Colors.black,
surface: Colors.black,
onPrimary: Colors.black,
onSecondary: Colors.black,
);
}
final SnackBarThemeData snackBarTheme = SnackBarThemeData( final SnackBarThemeData snackBarTheme = SnackBarThemeData(
actionTextColor: lightColorScheme.primary, actionTextColor: lightColorScheme.primary,
backgroundColor: lightColorScheme.secondaryContainer, backgroundColor: lightColorScheme.secondaryContainer,
@ -255,13 +270,13 @@ class BuildMainApp extends StatelessWidget {
title: 'PiliPala', title: 'PiliPala',
theme: buildThemeData( theme: buildThemeData(
currentThemeValue == ThemeType.dark currentThemeValue == ThemeType.dark
? darkColorScheme ? pureDarkColorScheme ?? darkColorScheme
: lightColorScheme, : lightColorScheme,
), ),
darkTheme: buildThemeData( darkTheme: buildThemeData(
currentThemeValue == ThemeType.light currentThemeValue == ThemeType.light
? lightColorScheme ? lightColorScheme
: darkColorScheme, : pureDarkColorScheme ?? darkColorScheme,
), ),
localizationsDelegates: const [ localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,

View File

@ -117,20 +117,23 @@ class DynamicsController extends GetxController {
/// 点击评论action 直接查看评论 /// 点击评论action 直接查看评论
if (action == 'comment') { if (action == 'comment') {
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor, 'action': action}); arguments: {'item': item, 'floor': floor, 'action': action},
preventDuplicates: false);
return false; return false;
} }
switch (item!.type) { switch (item!.type) {
/// 转发的动态 /// 转发的动态
case 'DYNAMIC_TYPE_FORWARD': case 'DYNAMIC_TYPE_FORWARD':
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor}); arguments: {'item': item, 'floor': floor},
preventDuplicates: false);
break; break;
/// 图文动态查看 /// 图文动态查看
case 'DYNAMIC_TYPE_DRAW': case 'DYNAMIC_TYPE_DRAW':
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor}); arguments: {'item': item, 'floor': floor},
preventDuplicates: false);
break; break;
case 'DYNAMIC_TYPE_AV': case 'DYNAMIC_TYPE_AV':
String bvid = item.modules.moduleDynamic.major.archive.bvid; String bvid = item.modules.moduleDynamic.major.archive.bvid;
@ -188,7 +191,8 @@ class DynamicsController extends GetxController {
case 'DYNAMIC_TYPE_WORD': case 'DYNAMIC_TYPE_WORD':
print('纯文本'); print('纯文本');
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor}); arguments: {'item': item, 'floor': floor},
preventDuplicates: false);
break; break;
case 'DYNAMIC_TYPE_LIVE_RCMD': case 'DYNAMIC_TYPE_LIVE_RCMD':
DynamicLiveModel liveRcmd = item.modules.moduleDynamic.major.liveRcmd; DynamicLiveModel liveRcmd = item.modules.moduleDynamic.major.liveRcmd;

View File

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -61,6 +63,7 @@ class _OverlayPanelState extends State<OverlayPanel>
void onClickUp(data, i, {type = 'click'}) { void onClickUp(data, i, {type = 'click'}) {
if (type == 'click') { if (type == 'click') {
data.hasUpdate = false;
pageController.jumpToPage(i); pageController.jumpToPage(i);
} }
} }
@ -81,11 +84,11 @@ class _OverlayPanelState extends State<OverlayPanel>
color: Colors.transparent, color: Colors.transparent,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Column( child: BackdropFilter(
children: [ filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
SizedBox( child: Column(
height: 50, children: [
child: TabBar( TabBar(
controller: _tabController, controller: _tabController,
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
automaticIndicatorColorAdjustment: false, automaticIndicatorColorAdjustment: false,
@ -106,25 +109,26 @@ class _OverlayPanelState extends State<OverlayPanel>
}); });
}, },
), ),
), Expanded(
Expanded( child: PageView.builder(
child: PageView.builder( itemCount: upList.length,
itemCount: upList.length, controller: pageController,
controller: pageController, itemBuilder: (BuildContext context, int index) {
itemBuilder: (BuildContext context, int index) { return Container(
return Container( clipBehavior: Clip.antiAlias,
clipBehavior: Clip.antiAlias, margin: const EdgeInsets.fromLTRB(10, 12, 10, 0),
margin: const EdgeInsets.fromLTRB(10, 12, 10, 0), decoration: BoxDecoration(
decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface,
color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(20), ),
), child:
child: UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr), UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr),
); );
}, },
),
), ),
), ],
], ),
), ),
); );
} }
@ -139,8 +143,8 @@ class _OverlayPanelState extends State<OverlayPanel>
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
scale: currentMid == data.mid ? 1 : 0.9, scale: currentMid == data.mid ? 1 : 0.9,
child: NetworkImgLayer( child: NetworkImgLayer(
width: contentWidth, width: 46,
height: contentWidth, height: 46,
src: data.face, src: data.face,
type: 'avatar', type: 'avatar',
), ),

View File

@ -44,6 +44,7 @@ class _UpPanelState extends State<UpPanel> {
void onClickUp(data, i) { void onClickUp(data, i) {
currentMid.value = data.mid; currentMid.value = data.mid;
data.hasUpdate = false;
Navigator.push( Navigator.push(
context, context,
PlPopupRoute( PlPopupRoute(

View File

@ -3,8 +3,8 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/pages/follow/index.dart'; import 'package:pilipala/pages/follow/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/group_panel.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/follow.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class FollowItem extends StatelessWidget { class FollowItem extends StatelessWidget {
@ -47,28 +47,11 @@ class FollowItem extends StatelessWidget {
height: 34, height: 34,
child: TextButton( child: TextButton(
onPressed: () async { onPressed: () async {
await showModalBottomSheet( int followStatus = await FollowUtils(
context: context, context: context,
useSafeArea: true, followStatus: 2,
isScrollControlled: true, mid: item.mid!,
builder: (BuildContext context) { ).showFollowSheet();
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: const [0.6],
builder: (BuildContext context,
ScrollController scrollController) {
return GroupPanel(
mid: item.mid!,
scrollController: scrollController,
);
},
);
},
);
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),

View File

@ -1,14 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/login.dart'; import 'package:pilipala/http/login.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
import 'package:pilipala/models/login/index.dart'; import 'package:pilipala/models/login/index.dart';
import 'package:pilipala/utils/login.dart'; import 'package:pilipala/utils/login.dart';
import 'package:pilipala/utils/utils.dart';
class LoginPageController extends GetxController { class LoginPageController extends GetxController {
final GlobalKey mobFormKey = GlobalKey<FormState>(); final GlobalKey mobFormKey = GlobalKey<FormState>();
@ -341,4 +346,32 @@ class LoginPageController extends GetxController {
Get.back(); Get.back();
} }
} }
// cookie登录
Future loginInByCookie({
required String cookiesStr,
String domain = HttpString.baseUrl,
}) async {
final List<String> cookiesStrList = cookiesStr.split('; ');
final List<Cookie> cookiesList = cookiesStrList.map((cookie) {
final cookieArr = cookie.split('=');
return Cookie(cookieArr[0], cookieArr[1]);
}).toList();
final String cookiePath = await Utils.getCookiePath();
final cookieJar = PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(cookiePath),
);
CookieManager cookieManager = CookieManager(cookieJar);
Request.cookieManager = cookieManager;
await Request.cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), cookiesList);
try {
Request.dio.options.headers['cookie'] = cookiesStr;
} catch (err) {
debugPrint(err.toString());
}
LoginUtils.confirmLogin('', null);
}
} }

View File

@ -14,6 +14,144 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> { class _LoginPageState extends State<LoginPage> {
final LoginPageController _loginPageCtr = Get.put(LoginPageController()); final LoginPageController _loginPageCtr = Get.put(LoginPageController());
// 浏览器登录
void loginInByWeb() {
Get.offNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
}
// 二维码方式登录
void loginInByWebQrcode() {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, StateSetter setState) {
return AlertDialog(
title: Row(
children: [
const Text('扫码登录'),
IconButton(
onPressed: () {
setState(() {});
},
icon: const Icon(Icons.refresh),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
content: AspectRatio(
aspectRatio: 1,
child: Container(
width: 200,
padding: const EdgeInsets.all(12),
child: FutureBuilder(
future: _loginPageCtr.getWebQrcode(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
return QrImageView(
data: data['data']['url'],
backgroundColor: Colors.white,
);
} else {
return const Center(
child: SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(),
),
);
}
},
),
),
),
actions: [
TextButton(
onPressed: () {},
child: Obx(() {
return Text(
'有效期: ${_loginPageCtr.validSeconds.value}s',
style: Theme.of(context).textTheme.titleMedium,
);
}),
),
TextButton(
onPressed: () {},
child: Text(
'检查登录状态',
style: TextStyle(
fontSize: Theme.of(context).textTheme.titleMedium!.fontSize,
),
),
)
],
);
});
},
).then((value) {
_loginPageCtr.validTimer!.cancel();
});
}
// cookie登录
// cookie登录
void loginInByCookie() async {
var cookies = '';
final outline = Theme.of(context).colorScheme.outline;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Cookie登录'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('请将主站cookie粘贴到下方输入框中点击「确认」即可完成登录。记得清空粘贴板'),
const SizedBox(height: 12),
TextField(
minLines: 1,
maxLines: 3,
decoration: InputDecoration(
labelText: 'cookie',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
onChanged: (e) => cookies = e,
),
],
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text('取消', style: TextStyle(color: outline))),
TextButton(
onPressed: () async {
if (cookies.isEmpty) {
return;
}
await _loginPageCtr.loginInByCookie(cookiesStr: cookies);
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: const Text('确认'))
],
);
},
);
}
@override @override
void dispose() { void dispose() {
_loginPageCtr.validTimer?.cancel(); _loginPageCtr.validTimer?.cancel();
@ -43,100 +181,17 @@ class _LoginPageState extends State<LoginPage> {
actions: [ actions: [
IconButton( IconButton(
tooltip: '浏览器打开', tooltip: '浏览器打开',
onPressed: () { onPressed: loginInByWeb,
Get.offNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
icon: const Icon(Icons.language, size: 20), icon: const Icon(Icons.language, size: 20),
), ),
IconButton(
tooltip: 'cookie登录',
onPressed: loginInByCookie,
icon: const Icon(Icons.cookie_outlined, size: 20),
),
IconButton( IconButton(
tooltip: '二维码登录', tooltip: '二维码登录',
onPressed: () { onPressed: loginInByWebQrcode,
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return AlertDialog(
title: Row(
children: [
const Text('扫码登录'),
IconButton(
onPressed: () {
setState(() {});
},
icon: const Icon(Icons.refresh),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
content: AspectRatio(
aspectRatio: 1,
child: Container(
width: 200,
padding: const EdgeInsets.all(12),
child: FutureBuilder(
future: _loginPageCtr.getWebQrcode(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
return QrImageView(
data: data['data']['url'],
backgroundColor: Colors.white,
);
} else {
return const Center(
child: SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(),
),
);
}
},
),
),
),
actions: [
TextButton(
onPressed: () {},
child: Obx(() {
return Text(
'有效期: ${_loginPageCtr.validSeconds.value}s',
style: Theme.of(context).textTheme.titleMedium,
);
}),
),
TextButton(
onPressed: () {},
child: Text(
'检查登录状态',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
),
),
)
],
);
});
},
).then((value) {
_loginPageCtr.validTimer!.cancel();
});
},
icon: const Icon(Icons.qr_code, size: 20), icon: const Icon(Icons.qr_code, size: 20),
), ),
const SizedBox(width: 22), const SizedBox(width: 22),

View File

@ -1,15 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart'; import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/like.dart'; import 'package:pilipala/models/member/like.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/follow.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@ -29,6 +28,12 @@ class MemberController extends GetxController {
RxList<MemberCoinsDataModel> recentCoinsList = <MemberCoinsDataModel>[].obs; RxList<MemberCoinsDataModel> recentCoinsList = <MemberCoinsDataModel>[].obs;
RxList<MemberLikeDataModel> recentLikeList = <MemberLikeDataModel>[].obs; RxList<MemberLikeDataModel> recentLikeList = <MemberLikeDataModel>[].obs;
RxBool isOwner = false.obs; RxBool isOwner = false.obs;
final Map<int, String> attributeTextMap = {
1: '悄悄关注',
2: '已关注',
6: '已互粉',
128: '已拉黑',
};
@override @override
void onInit() { void onInit() {
@ -85,11 +90,12 @@ class MemberController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
if (attribute.value == 128) { attribute.value = await FollowUtils(
modifyRelation('block'); context: Get.context!,
} else { followStatus: attribute.value,
modifyRelation('follow'); mid: mid,
} ).showFollowSheet();
attributeText.value = attributeTextMap[attribute.value] ?? '关注';
} }
// 关系查询 // 关系查询
@ -99,16 +105,10 @@ class MemberController extends GetxController {
var res = await UserHttp.hasFollow(mid); var res = await UserHttp.hasFollow(mid);
if (res['status']) { if (res['status']) {
attribute.value = res['data']['attribute']; attribute.value = res['data']['attribute'];
final Map<int, String> attributeTextMap = {
1: '悄悄关注',
2: '已关注',
6: '已互关',
128: '已拉黑',
};
attributeText.value = attributeTextMap[attribute.value] ?? '关注';
if (res['data']['special'] == 1) { if (res['data']['special'] == 1) {
attributeText.value = '特别关注'; attributeText.value = '特别关注';
} }
attributeText.value = attributeTextMap[attribute.value] ?? '关注';
} }
} }
@ -118,66 +118,15 @@ class MemberController extends GetxController {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
modifyRelation('block'); final String actionType = attribute.value == 128 ? 'remove' : 'block';
} attribute.value = await FollowUtils(
// 合并关注/取关和拉黑逻辑
Future modifyRelation(String actionType) async {
String contentText;
int act;
if (actionType == 'follow') {
contentText = memberInfo.value.isFollowed! ? '确定取消关注UP主?' : '确定关注UP主?';
act = memberInfo.value.isFollowed! ? 2 : 1;
} else if (actionType == 'block') {
contentText = attribute.value != 128 ? '确定拉黑UP主?' : '确定从黑名单移除UP主?';
act = attribute.value != 128 ? 5 : 6;
} else {
return;
}
showDialog(
context: Get.context!, context: Get.context!,
builder: (BuildContext context) { followStatus: attribute.value,
return AlertDialog( mid: mid,
title: const Text('提示'), ).modifyRelationFetch(actionType, isDirect: true);
content: Text(contentText), if (attribute.value != -1) {
actions: [ attributeText.value = attributeTextMap[attribute.value] ?? '关注';
TextButton( }
onPressed: () => Navigator.of(context).pop(),
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: mid,
act: act,
reSrc: 11,
);
SmartDialog.dismiss();
if (res['status']) {
if (actionType == 'follow') {
memberInfo.value.isFollowed = !memberInfo.value.isFollowed!;
} else if (actionType == 'block') {
attribute.value = attribute.value != 128 ? 128 : 0;
attributeText.value = attribute.value == 128 ? '已拉黑' : '关注';
memberInfo.value.isFollowed = false;
}
relationSearch();
if (context.mounted) {
Navigator.of(context).pop();
}
memberInfo.update((val) {});
}
},
child: const Text('确定'),
)
],
);
},
);
} }
void shareUser() { void shareUser() {

View File

@ -37,6 +37,7 @@ class MemberSearchController extends GetxController {
} else { } else {
Get.back(); Get.back();
} }
loadingStatus.value = 'init';
} }
void onChange(value) { void onChange(value) {
@ -76,7 +77,7 @@ class MemberSearchController extends GetxController {
archivePn += 1; archivePn += 1;
hasRequest = true; hasRequest = true;
} }
// loadingStatus.value = 'finish'; loadingStatus.value = 'finish';
return res; return res;
} }

View File

@ -249,6 +249,15 @@ class _StyleSettingState extends State<StyleSetting> {
'当前模式:${settingController.themeType.value.description}', '当前模式:${settingController.themeType.value.description}',
style: subTitleStyle)), style: subTitleStyle)),
), ),
SetSwitchItem(
title: '纯黑模式',
subTitle: '深色模式时使用纯黑色背景适用于OLED屏幕',
setKey: SettingBoxKey.enablePureBlack,
defaultVal: false,
callFn: (bool val) => {
if (val && Get.isDarkMode) {Get.appUpdate()}
},
),
ListTile( ListTile(
dense: false, dense: false,
onTap: () => settingController.setDynamicBadgeMode(context), onTap: () => settingController.setDynamicBadgeMode(context),

View File

@ -17,6 +17,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/follow.dart';
import 'package:pilipala/utils/global_data_cache.dart'; import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -26,7 +27,6 @@ import '../../../../common/pages_bottom_sheet.dart';
import '../../../../models/common/video_episode_type.dart'; import '../../../../models/common/video_episode_type.dart';
import '../../../../utils/drawer.dart'; import '../../../../utils/drawer.dart';
import '../related/index.dart'; import '../related/index.dart';
import 'widgets/group_panel.dart';
class VideoIntroController extends GetxController { class VideoIntroController extends GetxController {
VideoIntroController({required this.bvid}); VideoIntroController({required this.bvid});
@ -50,7 +50,7 @@ class VideoIntroController extends GetxController {
List addMediaIdsNew = []; List addMediaIdsNew = [];
List delMediaIdsNew = []; List delMediaIdsNew = [];
// 关注状态 默认未关注 // 关注状态 默认未关注
RxMap followStatus = {}.obs; RxInt followStatus = (-1).obs;
RxInt lastPlayCid = 0.obs; RxInt lastPlayCid = 0.obs;
UserInfoData? userInfo; UserInfoData? userInfo;
RxList<VideoTagItem> videoTags = <VideoTagItem>[].obs; RxList<VideoTagItem> videoTags = <VideoTagItem>[].obs;
@ -66,6 +66,8 @@ class VideoIntroController extends GetxController {
late bool enableRelatedVideo; late bool enableRelatedVideo;
UgcSeason? ugcSeason; UgcSeason? ugcSeason;
RxList<Part> pages = <Part>[].obs; RxList<Part> pages = <Part>[].obs;
// 默认原创视频
int copyright = 1;
@override @override
void onInit() { void onInit() {
@ -94,6 +96,7 @@ class VideoIntroController extends GetxController {
videoDetail.value = result['data']!; videoDetail.value = result['data']!;
ugcSeason = result['data']!.ugcSeason; ugcSeason = result['data']!.ugcSeason;
pages.value = result['data']!.pages!; pages.value = result['data']!.pages!;
copyright = result['data']!.copyright!;
if (type == null) { if (type == null) {
lastPlayCid.value = cid ?? videoDetail.value.cid!; lastPlayCid.value = cid ?? videoDetail.value.cid!;
} }
@ -215,7 +218,7 @@ class VideoIntroController extends GetxController {
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [1, 2] children: (copyright == 2 ? [1] : [1, 2])
.map( .map(
(e) => ListTile( (e) => ListTile(
title: Padding( title: Padding(
@ -334,113 +337,23 @@ class VideoIntroController extends GetxController {
} }
var result = await VideoHttp.hasFollow(mid: videoDetail.value.owner!.mid!); var result = await VideoHttp.hasFollow(mid: videoDetail.value.owner!.mid!);
if (result['status']) { if (result['status']) {
followStatus.value = result['data']; followStatus.value = result['data']['attribute'];
} }
return result; return result;
} }
// 关注/取关up // 关注/取关up
Future actionRelationMod() async { Future actionRelationMod(BuildContext context) async {
feedBack(); feedBack();
if (userInfo == null) { if (userInfo == null) {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
final int currentStatus = followStatus['attribute']; followStatus.value = await FollowUtils(
if (currentStatus == 128) { context: context,
modifyRelation('block', currentStatus); followStatus: followStatus.value,
} else { mid: videoDetail.value.owner!.mid!,
modifyRelation('follow', currentStatus); ).showFollowSheet();
}
}
// 操作用户关系
Future modifyRelation(String actionType, int currentStatus) async {
final int mid = videoDetail.value.owner!.mid!;
String contentText;
int act;
if (actionType == 'follow') {
contentText = currentStatus != 0 ? '确定取消关注UP主?' : '确定关注UP主?';
act = currentStatus != 0 ? 2 : 1;
} else if (actionType == 'block') {
contentText = '确定从黑名单移除UP主?';
act = 6;
} else {
return;
}
showDialog(
context: Get.context!,
builder: (BuildContext context) {
final Color outline = Theme.of(Get.context!).colorScheme.outline;
return AlertDialog(
title: const Text('提示'),
content: Text(contentText),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text('点错了', style: TextStyle(color: outline)),
),
TextButton(
onPressed: () => modifyRelationFetch(
context,
mid,
act,
currentStatus,
actionType,
),
child: const Text('确定'),
)
],
);
},
);
}
// 操作用户关系Future
Future modifyRelationFetch(
BuildContext context,
mid,
act,
currentStatus,
actionType,
) async {
var res = await VideoHttp.relationMod(mid: mid, act: act, reSrc: 11);
if (context.mounted) {
Navigator.of(context).pop();
}
if (res['status']) {
if (actionType == 'follow') {
final Map<int, int> statusMap = {
0: 2,
2: 0,
};
late int actionStatus;
actionStatus = statusMap[currentStatus] ?? 0;
followStatus['attribute'] = actionStatus;
if (currentStatus == 0 && Get.context!.mounted) {
ScaffoldMessenger.of(Get.context!).showSnackBar(
SnackBar(
content: const Text('关注成功'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: '设置分组',
onPressed: setFollowGroup,
),
showCloseIcon: true,
),
);
} else {
SmartDialog.showToast('取消关注成功');
}
} else if (actionType == 'block') {
followStatus['attribute'] = 0;
SmartDialog.showToast('取消拉黑成功');
}
followStatus.refresh();
} else {
SmartDialog.showToast(res['msg']);
}
} }
// 修改分P或番剧分集 // 修改分P或番剧分集
@ -565,33 +478,33 @@ class VideoIntroController extends GetxController {
} }
// 设置关注分组 // 设置关注分组
void setFollowGroup() async { // void setFollowGroup() async {
final mediaQueryData = MediaQuery.of(Get.context!); // final mediaQueryData = MediaQuery.of(Get.context!);
final contentHeight = mediaQueryData.size.height - kToolbarHeight; // final contentHeight = mediaQueryData.size.height - kToolbarHeight;
final double initialChildSize = // final double initialChildSize =
(contentHeight - Get.width * 9 / 16) / contentHeight; // (contentHeight - Get.width * 9 / 16) / contentHeight;
await showModalBottomSheet( // await showModalBottomSheet(
context: Get.context!, // context: Get.context!,
useSafeArea: true, // useSafeArea: true,
isScrollControlled: true, // isScrollControlled: true,
builder: (BuildContext context) { // builder: (BuildContext context) {
return DraggableScrollableSheet( // return DraggableScrollableSheet(
initialChildSize: initialChildSize, // initialChildSize: initialChildSize,
minChildSize: 0, // minChildSize: 0,
maxChildSize: 1, // maxChildSize: 1,
snap: true, // snap: true,
expand: false, // expand: false,
snapSizes: [initialChildSize], // snapSizes: [initialChildSize],
builder: (BuildContext context, ScrollController scrollController) { // builder: (BuildContext context, ScrollController scrollController) {
return GroupPanel( // return GroupPanel(
mid: videoDetail.value.owner!.mid!, // mid: videoDetail.value.owner!.mid!,
scrollController: scrollController, // scrollController: scrollController,
); // );
}, // },
); // );
}, // },
); // );
} // }
// ai总结 // ai总结
Future aiConclusion() async { Future aiConclusion() async {

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_intro.dart'; import 'package:pilipala/common/skeleton/video_intro.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -17,6 +18,7 @@ import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/introduction/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/controller.dart';
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart'; import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/follow.dart';
import 'package:pilipala/utils/global_data_cache.dart'; import 'package:pilipala/utils/global_data_cache.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';
@ -264,6 +266,26 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData t = Theme.of(context); final ThemeData t = Theme.of(context);
final Color outline = t.colorScheme.outline; final Color outline = t.colorScheme.outline;
const TextStyle titleStyle = TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
);
TextSpan titltWidget = TextSpan(
children: [
WidgetSpan(
child: Visibility(
visible: widget.videoDetail!.copyright == 2,
child: const PBadge(text: '转载', type: 'color'),
),
),
const TextSpan(text: ' '),
TextSpan(
text: widget.videoDetail!.title!,
style: titleStyle,
),
],
);
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: StyleString.safeSpace, left: StyleString.safeSpace,
@ -285,25 +307,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
}, },
child: ExpandablePanel( child: ExpandablePanel(
controller: _expandableCtr, controller: _expandableCtr,
collapsed: Text( collapsed: Text.rich(softWrap: true, maxLines: 2, titltWidget),
widget.videoDetail!.title!, expanded: Text.rich(softWrap: true, maxLines: 10, titltWidget),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
expanded: Text(
widget.videoDetail!.title!,
softWrap: true,
maxLines: 10,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
theme: const ExpandableThemeData( theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300), animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300), scrollAnimationDuration: Duration(milliseconds: 300),
@ -454,14 +459,14 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Obx( Obx(
() { () {
final int attr = final int attr =
videoIntroController.followStatus['attribute'] ?? 0; videoIntroController.followStatus.value;
return videoIntroController.followStatus.isEmpty return attr == -1
? const SizedBox() ? const SizedBox()
: SizedBox( : SizedBox(
height: 32, height: 32,
child: TextButton( child: TextButton(
onPressed: onPressed: () => videoIntroController
videoIntroController.actionRelationMod, .actionRelationMod(context),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8, left: 8,

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
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';
@ -76,7 +77,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
getStatusHeight(); getStatusHeight();
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
vdCtr = Get.put(VideoDetailController(), tag: heroTag); vdCtr = Get.put(VideoDetailController(), tag: heroTag);
vdCtr.sheetHeight.value = localCache.get('sheetHeight'); vdCtr.sheetHeight.value = GlobalDataCache.sheetHeight;
videoIntroController = Get.put( videoIntroController = Get.put(
VideoIntroController(bvid: Get.parameters['bvid']!), VideoIntroController(bvid: Get.parameters['bvid']!),
tag: heroTag); tag: heroTag);
@ -223,8 +224,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void _extendNestCtrListener() { void _extendNestCtrListener() {
final double offset = _extendNestCtr.position.pixels; final double offset = _extendNestCtr.position.pixels;
if (vdCtr.videoDirection.value == 'horizontal') { if (vdCtr.videoDirection.value == 'horizontal') {
vdCtr.sheetHeight.value = vdCtr.sheetHeight.value = max(GlobalDataCache.sheetHeight,
Get.size.height - videoHeight - statusBarHeight + offset; Get.size.height - videoHeight - statusBarHeight + offset);
appbarStream.add(offset); appbarStream.add(offset);
} else { } else {
if (offset > (Get.size.width * 22 / 16 - videoHeight)) { if (offset > (Get.size.width * 22 / 16 - videoHeight)) {
@ -502,7 +503,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sizeContext = MediaQuery.sizeOf(context); final sizeContext = MediaQuery.sizeOf(context);
final _context = MediaQuery.of(context); final orientation = MediaQuery.orientationOf(context);
late final double verticalHeight = sizeContext.width * 22 / 16; late final double verticalHeight = sizeContext.width * 22 / 16;
late double defaultVideoHeight = vdCtr.videoDirection.value == 'vertical' late double defaultVideoHeight = vdCtr.videoDirection.value == 'vertical'
? verticalHeight ? verticalHeight
@ -517,12 +518,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}); });
// 竖屏 // 竖屏
final bool isPortrait = _context.orientation == Orientation.portrait; final bool isPortrait = orientation == Orientation.portrait;
// 横屏 // 横屏
final bool isLandscape = _context.orientation == Orientation.landscape; final bool isLandscape = orientation == Orientation.landscape;
final Rx<bool> isFullScreen = plPlayerController?.isFullScreen ?? false.obs; final Rx<bool> isFullScreen = plPlayerController?.isFullScreen ?? false.obs;
// 全屏时高度撑满 // 全屏时高度撑满
if (isLandscape || isFullScreen.value == true) { if (isLandscape || isFullScreen.value) {
videoHeight.value = Get.size.height; videoHeight.value = Get.size.height;
enterFullScreen(); enterFullScreen();
} else { } else {
@ -634,10 +635,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
Widget childWhenDisabled = SafeArea( Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait && top: isPortrait && isFullScreen.value,
plPlayerController?.isFullScreen.value == true, bottom: isPortrait && isFullScreen.value,
bottom: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true,
left: false, left: false,
right: false, right: false,
child: Stack( child: Stack(
@ -660,22 +659,25 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return <Widget>[ return <Widget>[
Obx( Obx(
() { () {
final Orientation orientation = final bool isLandscape =
MediaQuery.of(context).orientation; MediaQuery.orientationOf(context) ==
Orientation.landscape;
final bool isFullScreen = final bool isFullScreen =
plPlayerController?.isFullScreen.value == true; plPlayerController?.isFullScreen.value ?? false;
final double expandedHeight =
orientation == Orientation.landscape || isFullScreen late double expandedHeight;
? (MediaQuery.sizeOf(context).height - if (isLandscape || isFullScreen) {
(orientation == Orientation.landscape
? 0
: MediaQuery.of(context).padding.top))
: videoHeight.value;
if (orientation == Orientation.landscape ||
isFullScreen) {
enterFullScreen(); enterFullScreen();
expandedHeight = (MediaQuery.sizeOf(context).height -
(isLandscape
? 0
: MediaQuery.paddingOf(context).top));
} else { } else {
exitFullScreen(); exitFullScreen();
if (vdCtr.videoDirection.value == 'vertical') {
videoHeight.value = verticalHeight;
}
expandedHeight = videoHeight.value;
} }
return SliverAppBar( return SliverAppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
@ -687,21 +689,24 @@ class _VideoDetailPageState extends State<VideoDetailPage>
backgroundColor: Colors.black, backgroundColor: Colors.black,
flexibleSpace: SizedBox.expand( flexibleSpace: SizedBox.expand(
child: PopScope( child: PopScope(
canPop: canPop: !isFullScreen,
plPlayerController?.isFullScreen.value != true,
onPopInvoked: (bool didPop) { onPopInvoked: (bool didPop) {
if (plPlayerController?.controlsLock.value == if (plPlayerController != null) {
true) { if (plPlayerController!.controlsLock.value) {
plPlayerController?.onLockControl(false); plPlayerController!.onLockControl(false);
return; return;
}
if (isFullScreen) {
plPlayerController!
.triggerFullScreen(status: false);
if (vdCtr.videoDirection.value ==
'vertical') {
videoHeight.value = verticalHeight;
}
}
} }
if (plPlayerController?.isFullScreen.value ==
true) { if (isLandscape) {
plPlayerController!
.triggerFullScreen(status: false);
}
if (MediaQuery.of(context).orientation ==
Orientation.landscape) {
verticalScreen(); verticalScreen();
} }
}, },
@ -746,9 +751,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 不收回 /// 不收回
pinnedHeaderSliverHeightBuilder: () { pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).orientation == return isLandscape || isFullScreen.value
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height ? MediaQuery.sizeOf(context).height
: playerStatus.value != PlayerStatus.playing : playerStatus.value != PlayerStatus.playing
? kToolbarHeight ? kToolbarHeight

View File

@ -1068,13 +1068,10 @@ class _HeaderControlState extends State<HeaderControl> {
); );
final bool isLandscape = final bool isLandscape =
MediaQuery.of(context).orientation == Orientation.landscape; MediaQuery.of(context).orientation == Orientation.landscape;
return AppBar( return Padding(
backgroundColor: Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 14),
foregroundColor: Colors.white, child: Column(
primary: false, mainAxisSize: MainAxisSize.min,
automaticallyImplyLeading: false,
titleSpacing: 14,
title: Column(
children: [ children: [
if (isFullScreen.value && isLandscape) ...[ if (isFullScreen.value && isLandscape) ...[
Row( Row(

View File

@ -45,36 +45,44 @@ class WhisperController extends GetxController {
if (isLoading) return; if (isLoading) return;
var res = await MsgHttp.sessionList( var res = await MsgHttp.sessionList(
endTs: type == 'onLoad' ? sessionList.last.sessionTs : null); endTs: type == 'onLoad' ? sessionList.last.sessionTs : null);
if (res['status'] && try {
res['data'].sessionList != null && if (res['status'] &&
res['data'].sessionList.isNotEmpty) { res['data'].sessionList != null &&
await queryAccountList(res['data'].sessionList); res['data'].sessionList.isNotEmpty) {
// 将 accountList 转换为 Map 结构 await queryAccountList(res['data'].sessionList);
Map<int, dynamic> accountMap = {}; // 将 accountList 转换为 Map 结构
for (var j in accountList) { Map<int, dynamic> accountMap = {};
accountMap[j.mid!] = j; for (var j in accountList) {
} accountMap[j.mid!] = j;
}
// 遍历 sessionList通过 mid 查找并赋值 accountInfo // 遍历 sessionList通过 mid 查找并赋值 accountInfo
for (var i in res['data'].sessionList) { for (var i in res['data'].sessionList) {
var accountInfo = accountMap[i.talkerId]; var accountInfo = accountMap[i.talkerId];
if (accountInfo != null) { if (accountInfo != null) {
i.accountInfo = accountInfo; i.accountInfo = accountInfo;
}
if (i.talkerId == 844424930131966) {
i.accountInfo = AccountListModel(
name: 'UP主小助手',
face:
'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',
);
}
} }
if (i.talkerId == 844424930131966) { if (type == 'onLoad') {
i.accountInfo = AccountListModel( sessionList.addAll(res['data'].sessionList);
name: 'UP主小助手', } else {
face: sessionList.value = res['data'].sessionList;
'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',
);
} }
} }
if (type == 'onLoad') { } catch (err) {
sessionList.addAll(res['data'].sessionList); res = {
} else { 'status': false,
sessionList.value = res['data'].sessionList; 'message': err.toString(),
} };
} }
isLoading = false; isLoading = false;
return res; return res;
} }

View File

@ -44,7 +44,24 @@ class _WhisperPageState extends State<WhisperPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('消息')), appBar: AppBar(
title: const Text('消息'),
actions: [
IconButton(
icon: Icon(Icons.open_in_browser_rounded,
color: Theme.of(context).colorScheme.primary),
tooltip: '用浏览器打开',
onPressed: () {
Get.toNamed('/webview', parameters: {
'url': 'https://message.bilibili.com',
'type': 'whisper',
'pageTitle': '消息中心',
});
},
),
const SizedBox(width: 12)
],
),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
_whisperController.unread(); _whisperController.unread();

View File

@ -521,6 +521,7 @@ class SystemNotice extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Map content = item.content ?? ''; Map content = item.content ?? '';
Color primary = Theme.of(context).colorScheme.primary;
return Row( return Row(
children: [ children: [
const SizedBox(width: 12), const SizedBox(width: 12),
@ -557,12 +558,35 @@ class SystemNotice extends StatelessWidget {
.labelSmall! .labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline), .copyWith(color: Theme.of(context).colorScheme.outline),
), ),
Divider( Divider(color: primary.withOpacity(0.05)),
color: Theme.of(context).colorScheme.primary.withOpacity(0.05), SelectableText(content['text']),
), if (content['jump_text'] != null &&
SelectableText( content['jump_uri'] != null) ...[
content['text'], Divider(color: primary.withOpacity(0.05)),
) Align(
alignment: Alignment.center,
child: TextButton(
onPressed: () {
Get.toNamed('/webview', parameters: {
'url': content['jump_uri'],
'type': 'url',
'pageTitle': content['jump_text'] == ''
? '查看详情'
: content['jump_text'],
});
},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith((states) {
return primary.withAlpha(20);
}),
),
child: Text(content['jump_text'] == ''
? '查看详情'
: content['jump_text']),
),
),
]
], ],
), ),
), ),

View File

@ -443,334 +443,336 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
color: Colors.white, color: Colors.white,
fontSize: 12, fontSize: 12,
); );
return Stack( return ClipRect(
fit: StackFit.passthrough, child: Stack(
children: <Widget>[ fit: StackFit.passthrough,
Obx( children: <Widget>[
() => Video( Obx(
key: ValueKey(_.videoFit.value), () => Video(
controller: videoController, key: ValueKey(_.videoFit.value),
controls: NoVideoControls, controller: videoController,
alignment: widget.alignment!, controls: NoVideoControls,
pauseUponEnteringBackgroundMode: !enableBackgroundPlay, alignment: widget.alignment!,
resumeUponEnteringForegroundMode: true, pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
subtitleViewConfiguration: const SubtitleViewConfiguration( resumeUponEnteringForegroundMode: true,
style: subTitleStyle, subtitleViewConfiguration: const SubtitleViewConfiguration(
padding: EdgeInsets.all(24.0), style: subTitleStyle,
padding: EdgeInsets.all(24.0),
),
fit: _.videoFit.value,
), ),
fit: _.videoFit.value,
), ),
),
/// 长按倍速 toast /// 长按倍速 toast
Obx( Obx(
() => Align( () => Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: FractionalTranslation( child: FractionalTranslation(
translation: const Offset(0.0, 0.3), // 上下偏移量(负数向上偏移) translation: const Offset(0.0, 0.3), // 上下偏移量(负数向上偏移)
child: AnimatedOpacity( child: AnimatedOpacity(
curve: Curves.easeInOut, curve: Curves.easeInOut,
opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0, opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0x88000000), color: const Color(0x88000000),
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
),
height: 32.0,
width: 70.0,
child: const Center(
child: Text(
'倍速中',
style: TextStyle(color: Colors.white, fontSize: 13),
), ),
)), height: 32.0,
width: 70.0,
child: const Center(
child: Text(
'倍速中',
style: TextStyle(color: Colors.white, fontSize: 13),
),
)),
),
), ),
), ),
), ),
),
/// 时间进度 toast /// 时间进度 toast
Obx( Obx(
() => Align( () => Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: FractionalTranslation( child: FractionalTranslation(
translation: const Offset(0.0, 1.0), // 上下偏移量(负数向上偏移) translation: const Offset(0.0, 1.0), // 上下偏移量(负数向上偏移)
child: AnimatedOpacity( child: AnimatedOpacity(
curve: Curves.easeInOut, curve: Curves.easeInOut,
opacity: _.isSliderMoving.value ? 1.0 : 0.0, opacity: _.isSliderMoving.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: IntrinsicWidth( child: IntrinsicWidth(
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0x88000000), color: const Color(0x88000000),
borderRadius: BorderRadius.circular(64.0), borderRadius: BorderRadius.circular(64.0),
), ),
height: 34.0, height: 34.0,
padding: const EdgeInsets.only(left: 10, right: 10), padding: const EdgeInsets.only(left: 10, right: 10),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Obx(() { Obx(() {
return Text( return Text(
_.sliderTempPosition.value.inMinutes >= 60 _.sliderTempPosition.value.inMinutes >= 60
? printDurationWithHours( ? printDurationWithHours(
_.sliderTempPosition.value) _.sliderTempPosition.value)
: printDuration(_.sliderTempPosition.value), : printDuration(_.sliderTempPosition.value),
style: textStyle, style: textStyle,
); );
}), }),
const SizedBox(width: 2), const SizedBox(width: 2),
const Text('/', style: textStyle), const Text('/', style: textStyle),
const SizedBox(width: 2), const SizedBox(width: 2),
Obx( Obx(
() => Text( () => Text(
_.duration.value.inMinutes >= 60 _.duration.value.inMinutes >= 60
? printDurationWithHours(_.duration.value) ? printDurationWithHours(_.duration.value)
: printDuration(_.duration.value), : printDuration(_.duration.value),
style: textStyle, style: textStyle,
),
), ),
), ],
], ),
), ),
), ),
), ),
), ),
), ),
), ),
),
/// 音量🔊 控制条展示 /// 音量🔊 控制条展示
Obx( Obx(
() => ControlBar( () => ControlBar(
visible: _volumeIndicator.value, visible: _volumeIndicator.value,
icon: _volumeValue.value < 1.0 / 3.0 icon: _volumeValue.value < 1.0 / 3.0
? Icons.volume_mute ? Icons.volume_mute
: _volumeValue.value < 2.0 / 3.0 : _volumeValue.value < 2.0 / 3.0
? Icons.volume_down ? Icons.volume_down
: Icons.volume_up, : Icons.volume_up,
value: _volumeValue.value, value: _volumeValue.value,
),
), ),
),
/// 亮度🌞 控制条展示 /// 亮度🌞 控制条展示
Obx( Obx(
() => ControlBar( () => ControlBar(
visible: _brightnessIndicator.value, visible: _brightnessIndicator.value,
icon: _brightnessValue.value < 1.0 / 3.0 icon: _brightnessValue.value < 1.0 / 3.0
? Icons.brightness_low ? Icons.brightness_low
: _brightnessValue.value < 2.0 / 3.0 : _brightnessValue.value < 2.0 / 3.0
? Icons.brightness_medium ? Icons.brightness_medium
: Icons.brightness_high, : Icons.brightness_high,
value: _brightnessValue.value, value: _brightnessValue.value,
),
), ),
),
// Obx(() { // Obx(() {
// if (_.buffered.value == Duration.zero) { // if (_.buffered.value == Duration.zero) {
// return Positioned.fill( // return Positioned.fill(
// child: Container( // child: Container(
// color: Colors.black, // color: Colors.black,
// child: Center( // child: Center(
// child: Image.asset( // child: Image.asset(
// 'assets/images/loading.gif', // 'assets/images/loading.gif',
// height: 25, // height: 25,
// ), // ),
// ), // ),
// ), // ),
// ); // );
// } else { // } else {
// return Container(); // return Container();
// } // }
// }), // }),
/// 弹幕面板 /// 弹幕面板
if (widget.danmuWidget != null) if (widget.danmuWidget != null)
Positioned.fill(top: 4, child: widget.danmuWidget!), Positioned.fill(top: 4, child: widget.danmuWidget!),
/// 开启且有字幕时展示 /// 开启且有字幕时展示
Stack( Stack(
children: [ children: [
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
bottom: 30, bottom: 30,
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Obx( child: Obx(
() => Visibility( () => Visibility(
visible: widget.controller.subTitleCode.value != -1, visible: widget.controller.subTitleCode.value != -1,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
color: widget.controller.subtitleContent.value != '' color: widget.controller.subtitleContent.value != ''
? Colors.black.withOpacity(0.6) ? Colors.black.withOpacity(0.6)
: Colors.transparent, : Colors.transparent,
),
padding: widget.controller.subTitleCode.value != -1
? const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
)
: EdgeInsets.zero,
child: Text(
widget.controller.subtitleContent.value,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
), ),
), padding: widget.controller.subTitleCode.value != -1
)), ? const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
)
: EdgeInsets.zero,
child: Text(
widget.controller.subtitleContent.value,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
)),
),
), ),
), ),
), ],
],
),
/// 手势
Positioned.fill(
left: 16,
top: 25,
right: 15,
bottom: 15,
child: GestureDetector(
onTap: () {
_.controls = !_.showControls.value;
},
onDoubleTapDown: (TapDownDetails details) {
// live模式下禁用 锁定时🔒禁用
if (_.videoType == 'live' || _.controlsLock.value) {
return;
}
final double totalWidth = MediaQuery.sizeOf(context).width;
final double tapPosition = details.localPosition.dx;
final double sectionWidth = totalWidth / 3;
String type = 'left';
if (tapPosition < sectionWidth) {
type = 'left';
} else if (tapPosition < sectionWidth * 2) {
type = 'center';
} else {
type = 'right';
}
doubleTapFuc(type);
},
onLongPressStart: (LongPressStartDetails detail) {
feedBack();
_.setDoubleSpeedStatus(true);
},
onLongPressEnd: (LongPressEndDetails details) {
_.setDoubleSpeedStatus(false);
},
/// 水平位置 快进 live模式下禁用
onHorizontalDragUpdate: (DragUpdateDetails details) {
// live模式下禁用 锁定时🔒禁用
if (_.videoType == 'live' || _.controlsLock.value) {
return;
}
// final double tapPosition = details.localPosition.dx;
final int curSliderPosition =
_.sliderPosition.value.inMilliseconds;
final double scale = 90000 / MediaQuery.sizeOf(context).width;
final Duration pos = Duration(
milliseconds:
curSliderPosition + (details.delta.dx * scale).round());
final Duration result =
pos.clamp(Duration.zero, _.duration.value);
_.onUpdatedSliderProgress(result);
_.onChangedSliderStart();
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_.videoType == 'live' || _.controlsLock.value) {
return;
}
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value, type: 'slider');
},
// 垂直方向 音量/亮度调节
onVerticalDragUpdate: (DragUpdateDetails details) async {
final double totalWidth = MediaQuery.sizeOf(context).width;
final double tapPosition = details.localPosition.dx;
final double sectionWidth =
fullScreenGestureMode == FullScreenGestureMode.none
? totalWidth / 2
: totalWidth / 3;
final double delta = details.delta.dy;
/// 锁定时禁用
if (_.controlsLock.value) {
return;
}
if (lastFullScreenToggleTime != null &&
DateTime.now().difference(lastFullScreenToggleTime!) <
const Duration(milliseconds: 500)) {
return;
}
if (tapPosition < sectionWidth) {
// 左边区域 👈
final double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16) *
3;
final double brightness =
_brightnessValue.value - delta / level;
final double result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else if (isUsingFullScreenGestures(tapPosition, sectionWidth)) {
// 全屏
final double dy = details.delta.dy;
const double threshold = 7.0; // 滑动阈值
final bool flag = fullScreenGestureMode !=
FullScreenGestureMode.fromBottomtoTop;
if (dy > _distance.value &&
dy > threshold &&
!_.controlsLock.value) {
if (_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 下滑退出全屏
await widget.controller.triggerFullScreen(status: flag);
}
_distance.value = 0.0;
} else if (dy < _distance.value &&
dy < -threshold &&
!_.controlsLock.value) {
if (!_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 上滑进入全屏
await widget.controller.triggerFullScreen(status: !flag);
}
_distance.value = 0.0;
}
_distance.value = dy;
} else {
// 右边区域 👈
EasyThrottle.throttle(
'setVolume', const Duration(milliseconds: 20), () {
final double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16);
final double volume = _volumeValue.value -
double.parse(delta.toStringAsFixed(1)) / level;
final double result = volume.clamp(0.0, 1.0);
setVolume(result);
});
}
},
onVerticalDragEnd: (DragEndDetails details) {},
), ),
),
// 头部、底部控制条 /// 手势
Obx( Positioned.fill(
() => Column( left: 16,
mainAxisAlignment: MainAxisAlignment.spaceBetween, top: 25,
children: [ right: 15,
if (widget.headerControl != null || _.headerControl != null) ...[ bottom: 15,
Flexible( child: GestureDetector(
child: ClipRect( onTap: () {
_.controls = !_.showControls.value;
},
onDoubleTapDown: (TapDownDetails details) {
// live模式下禁用 锁定时🔒禁用
if (_.videoType == 'live' || _.controlsLock.value) {
return;
}
final double totalWidth = MediaQuery.sizeOf(context).width;
final double tapPosition = details.localPosition.dx;
final double sectionWidth = totalWidth / 3;
String type = 'left';
if (tapPosition < sectionWidth) {
type = 'left';
} else if (tapPosition < sectionWidth * 2) {
type = 'center';
} else {
type = 'right';
}
doubleTapFuc(type);
},
onLongPressStart: (LongPressStartDetails detail) {
feedBack();
_.setDoubleSpeedStatus(true);
},
onLongPressEnd: (LongPressEndDetails details) {
_.setDoubleSpeedStatus(false);
},
/// 水平位置 快进 live模式下禁用
onHorizontalDragUpdate: (DragUpdateDetails details) {
// live模式下禁用 锁定时🔒禁用
if (_.videoType == 'live' || _.controlsLock.value) {
return;
}
// final double tapPosition = details.localPosition.dx;
final int curSliderPosition =
_.sliderPosition.value.inMilliseconds;
final double scale = 90000 / MediaQuery.sizeOf(context).width;
final Duration pos = Duration(
milliseconds:
curSliderPosition + (details.delta.dx * scale).round());
final Duration result =
pos.clamp(Duration.zero, _.duration.value);
_.onUpdatedSliderProgress(result);
_.onChangedSliderStart();
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_.videoType == 'live' || _.controlsLock.value) {
return;
}
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value, type: 'slider');
},
// 垂直方向 音量/亮度调节
onVerticalDragUpdate: (DragUpdateDetails details) async {
final double totalWidth = MediaQuery.sizeOf(context).width;
final double tapPosition = details.localPosition.dx;
final double sectionWidth =
fullScreenGestureMode == FullScreenGestureMode.none
? totalWidth / 2
: totalWidth / 3;
final double delta = details.delta.dy;
/// 锁定时禁用
if (_.controlsLock.value) {
return;
}
if (lastFullScreenToggleTime != null &&
DateTime.now().difference(lastFullScreenToggleTime!) <
const Duration(milliseconds: 500)) {
return;
}
if (tapPosition < sectionWidth) {
// 左边区域 👈
final double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16) *
3;
final double brightness =
_brightnessValue.value - delta / level;
final double result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else if (isUsingFullScreenGestures(
tapPosition, sectionWidth)) {
// 全屏
final double dy = details.delta.dy;
const double threshold = 7.0; // 滑动阈值
final bool flag = fullScreenGestureMode !=
FullScreenGestureMode.fromBottomtoTop;
if (dy > _distance.value &&
dy > threshold &&
!_.controlsLock.value) {
if (_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 下滑退出全屏
await widget.controller.triggerFullScreen(status: flag);
}
_distance.value = 0.0;
} else if (dy < _distance.value &&
dy < -threshold &&
!_.controlsLock.value) {
if (!_.isFullScreen.value ^ flag) {
lastFullScreenToggleTime = DateTime.now();
// 上滑进入全屏
await widget.controller.triggerFullScreen(status: !flag);
}
_distance.value = 0.0;
}
_distance.value = dy;
} else {
// 右边区域 👈
EasyThrottle.throttle(
'setVolume', const Duration(milliseconds: 20), () {
final double level = (_.isFullScreen.value
? Get.size.height
: screenWidth * 9 / 16);
final double volume = _volumeValue.value -
double.parse(delta.toStringAsFixed(1)) / level;
final double result = volume.clamp(0.0, 1.0);
setVolume(result);
});
}
},
onVerticalDragEnd: (DragEndDetails details) {},
),
),
// 头部、底部控制条
Obx(
() => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.headerControl != null ||
_.headerControl != null) ...[
Flexible(
child: AppBarAni( child: AppBarAni(
controller: animationController, controller: animationController,
visible: !_.controlsLock.value && _.showControls.value, visible: !_.controlsLock.value && _.showControls.value,
@ -778,13 +780,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: widget.headerControl ?? _.headerControl!, child: widget.headerControl ?? _.headerControl!,
), ),
), ),
), ] else ...[
] else ...[ const SizedBox.shrink()
const SizedBox.shrink() ],
], Flexible(
Flexible( flex: _.videoType == 'live' ? 0 : 1,
flex: _.videoType == 'live' ? 0 : 1,
child: ClipRect(
child: AppBarAni( child: AppBarAni(
controller: animationController, controller: animationController,
visible: !_.controlsLock.value && _.showControls.value, visible: !_.controlsLock.value && _.showControls.value,
@ -797,141 +797,141 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
), ),
), ],
], ),
), ),
),
/// 进度条 live模式下禁用 /// 进度条 live模式下禁用
Obx( Obx(
() { () {
final int value = _.sliderPositionSeconds.value; final int value = _.sliderPositionSeconds.value;
final int max = _.durationSeconds.value; final int max = _.durationSeconds.value;
final int buffer = _.bufferedSeconds.value; final int buffer = _.bufferedSeconds.value;
if (_.showControls.value) { if (_.showControls.value) {
return Container(); return Container();
} }
if (defaultBtmProgressBehavior == if (defaultBtmProgressBehavior ==
BtmProgresBehavior.alwaysHide.code) { BtmProgresBehavior.alwaysHide.code) {
return const SizedBox(); return const SizedBox();
} }
if (defaultBtmProgressBehavior == if (defaultBtmProgressBehavior ==
BtmProgresBehavior.onlyShowFullScreen.code && BtmProgresBehavior.onlyShowFullScreen.code &&
!_.isFullScreen.value) { !_.isFullScreen.value) {
return const SizedBox(); return const SizedBox();
} else if (defaultBtmProgressBehavior == } else if (defaultBtmProgressBehavior ==
BtmProgresBehavior.onlyHideFullScreen.code && BtmProgresBehavior.onlyHideFullScreen.code &&
_.isFullScreen.value) { _.isFullScreen.value) {
return const SizedBox(); return const SizedBox();
} }
if (_.videoType == 'live') { if (_.videoType == 'live') {
return const SizedBox(); return const SizedBox();
} }
if (value > max || max <= 0) { if (value > max || max <= 0) {
return const SizedBox(); return const SizedBox();
} }
return Positioned( return Positioned(
bottom: -1.5, bottom: -1.5,
left: 0, left: 0,
right: 0, right: 0,
child: ProgressBar( child: ProgressBar(
progress: Duration(seconds: value), progress: Duration(seconds: value),
buffered: Duration(seconds: buffer), buffered: Duration(seconds: buffer),
total: Duration(seconds: max), total: Duration(seconds: max),
progressBarColor: colorTheme, progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2), baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor: Colors.white.withOpacity(0.6), bufferedBarColor: Colors.white.withOpacity(0.6),
timeLabelLocation: TimeLabelLocation.none, timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme, thumbColor: colorTheme,
barHeight: 3, barHeight: 3,
thumbRadius: 0.0, thumbRadius: 0.0,
// onDragStart: (duration) { // onDragStart: (duration) {
// _.onChangedSliderStart(); // _.onChangedSliderStart();
// }, // },
// onDragEnd: () { // onDragEnd: () {
// _.onChangedSliderEnd(); // _.onChangedSliderEnd();
// }, // },
// onDragUpdate: (details) { // onDragUpdate: (details) {
// print(details); // print(details);
// }, // },
// onSeek: (duration) { // onSeek: (duration) {
// feedBack(); // feedBack();
// _.onChangedSlider(duration.inSeconds.toDouble()); // _.onChangedSlider(duration.inSeconds.toDouble());
// _.seekTo(duration); // _.seekTo(duration);
// }, // },
), ),
// SlideTransition( // SlideTransition(
// position: Tween<Offset>( // position: Tween<Offset>(
// begin: Offset.zero, // begin: Offset.zero,
// end: const Offset(0, -1), // end: const Offset(0, -1),
// ).animate(CurvedAnimation( // ).animate(CurvedAnimation(
// parent: animationController, // parent: animationController,
// curve: Curves.easeInOut, // curve: Curves.easeInOut,
// )), // )),
// child: ), // child: ),
); );
}, },
), ),
// 锁 // 锁
Obx( Obx(
() => Visibility( () => Visibility(
visible: _.videoType != 'live' && _.isFullScreen.value, visible: _.videoType != 'live' && _.isFullScreen.value,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FractionalTranslation( child: FractionalTranslation(
translation: const Offset(1, 0.0), translation: const Offset(1, 0.0),
child: Visibility( child: Visibility(
visible: _.showControls.value, visible: _.showControls.value,
child: ComBtn( child: ComBtn(
icon: Icon( icon: Icon(
_.controlsLock.value _.controlsLock.value
? FontAwesomeIcons.lock ? FontAwesomeIcons.lock
: FontAwesomeIcons.lockOpen, : FontAwesomeIcons.lockOpen,
size: 15, size: 15,
color: Colors.white, color: Colors.white,
),
fuc: () => _.onLockControl(!_.controlsLock.value),
), ),
fuc: () => _.onLockControl(!_.controlsLock.value),
), ),
), ),
), ),
), ),
), ),
), //
// Obx(() {
Obx(() { if (_.dataStatus.loading || _.isBuffering.value) {
if (_.dataStatus.loading || _.isBuffering.value) { return Center(
return Center( child: Container(
child: Container( padding: const EdgeInsets.all(30),
padding: const EdgeInsets.all(30), decoration: const BoxDecoration(
decoration: const BoxDecoration( shape: BoxShape.circle,
shape: BoxShape.circle, gradient: RadialGradient(
gradient: RadialGradient( colors: [Colors.black26, Colors.transparent],
colors: [Colors.black26, Colors.transparent], ),
),
child: Lottie.asset(
'assets/loading.json',
width: 200,
), ),
), ),
child: Lottie.asset( );
'assets/loading.json', } else {
width: 200, return const SizedBox();
), }
), }),
);
} else {
return const SizedBox();
}
}),
/// 快进/快退面板 /// 快进/快退面板
SeekPanel( SeekPanel(
mountSeekBackwardButton: _mountSeekBackwardButton, mountSeekBackwardButton: _mountSeekBackwardButton,
mountSeekForwardButton: _mountSeekForwardButton, mountSeekForwardButton: _mountSeekForwardButton,
hideSeekBackwardButton: _hideSeekBackwardButton, hideSeekBackwardButton: _hideSeekBackwardButton,
hideSeekForwardButton: _hideSeekForwardButton, hideSeekForwardButton: _hideSeekForwardButton,
onSubmittedcb: _handleSubmittedCallback, onSubmittedcb: _handleSubmittedCallback,
), ),
], ],
),
); );
} }
} }

View File

@ -18,19 +18,18 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Padding(
color: Colors.transparent,
height: 90,
padding: const EdgeInsets.symmetric(horizontal: 18), padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(7, 0, 7, 6), padding: const EdgeInsets.fromLTRB(7, 0, 7, 4),
child: ProgressBarWidget(controller: controller!), child: ProgressBarWidget(controller: controller!),
), ),
Row(children: buildBottomControl!), Row(children: buildBottomControl!),
const SizedBox(height: 10), const SizedBox(height: 6),
], ],
), ),
); );

207
lib/utils/follow.dart Normal file
View File

@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/common/widgets/drag_handle.dart';
import 'package:pilipala/common/widgets/group_panel.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/utils/global_data_cache.dart';
class FollowUtils {
final BuildContext context;
final int followStatus;
final int mid;
FollowUtils({
required this.context,
required this.followStatus,
required this.mid,
});
// static final Map<int, Map<String, dynamic>> followMap = {
// // 未关注
// 0: {
// 'desc': '确定关注UP主?',
// // 1 关注 5 拉黑
// 'act': 1,
// },
// // 已关注
// 2: {
// 'desc': '确定取消关注UP主?',
// 'act': 2,
// },
// // 已互粉
// 6: {
// 'desc': '确定取消关注UP主?',
// 'act': 2,
// },
// // 已拉黑
// 128: {
// 'desc': '确定从黑名单移除UP主?',
// 'act': 6,
// },
// };
static final Map<String, Map<String, dynamic>> actionTypeMap = {
'remove': {
'desc': '确定从黑名单移除UP主?',
'tips': '已从黑名单移除',
'act': 6,
'followStatus': 0,
},
'unFollow': {
'desc': '确定取消关注UP主?',
'tips': '已取消关注',
'act': 2,
'followStatus': 0,
},
'follow': {
'desc': '确定关注UP主?',
'tips': '关注成功',
'act': 1,
'followStatus': 2,
},
'block': {
'desc': '确定拉黑UP主?',
'tips': '已拉黑',
'act': 5,
'followStatus': 128,
},
};
Future<int> showFollowSheet() async {
var res = await showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandle(),
if (followStatus == 128) ...[
ListTile(
leading: const Icon(Icons.remove_circle_outline_rounded),
onTap: () => modifyRelation('remove'),
title: const Text('从黑名单移除'),
),
],
if ([2, 6].contains(followStatus)) ...[
ListTile(
leading: const Icon(Icons.group_add_outlined),
onTap: () {
Navigator.of(context).pop();
setFollowGroup();
},
title: const Text('设置分组'),
),
ListTile(
leading: const Icon(Icons.heart_broken_outlined),
onTap: () => modifyRelation('unFollow'),
title: const Text('取消关注'),
),
],
if (followStatus == 0) ...[
ListTile(
leading: const Icon(Icons.favorite_border_rounded),
onTap: () => modifyRelation('follow'),
title: const Text('关注up主'),
),
ListTile(
leading: const Icon(Icons.block_rounded),
onTap: () => modifyRelation('block'),
title: const Text('拉黑up主'),
),
],
],
),
);
},
);
return res ?? followStatus;
}
// 操作用户关系
Future modifyRelation(String actionType) async {
showDialog(
context: context,
builder: (BuildContext context) {
final Color outline = Theme.of(context).colorScheme.outline;
return AlertDialog(
title: const Text('提示'),
content: Text(actionTypeMap[actionType]!['desc']),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text('点错了', style: TextStyle(color: outline)),
),
TextButton(
onPressed: () => modifyRelationFetch(actionType),
child: const Text('确定'),
)
],
);
},
);
}
// 操作用户关系Future
Future<int> modifyRelationFetch(String actionType, {bool? isDirect}) async {
if (isDirect != true) {
Navigator.of(context).pop();
}
SmartDialog.showLoading(msg: '请求中');
var res = await VideoHttp.relationMod(
mid: mid,
act: actionTypeMap[actionType]!['act'],
reSrc: 11,
);
SmartDialog.dismiss();
if (res['status']) {
final int newFollowStatus = actionTypeMap[actionType]!['followStatus'];
SmartDialog.showToast(actionTypeMap[actionType]!['tips']);
if (context.mounted) {
if (isDirect != true) {
Navigator.of(context).pop(newFollowStatus);
}
}
return newFollowStatus;
} else {
SmartDialog.showToast(res['msg']);
if (context.mounted && isDirect != true) {
Navigator.of(context).pop(-1);
}
return -1;
}
}
// 设置分组
Future setFollowGroup() async {
final size = MediaQuery.sizeOf(context);
final contentHeight = size.height - kToolbarHeight;
final double initialChildSize =
(contentHeight - size.width * 9 / 16) / contentHeight;
await showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
builder: (BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: initialChildSize,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: [initialChildSize],
builder: (BuildContext context, ScrollController scrollController) {
return GroupPanel(
mid: GlobalDataCache.userInfo!.mid!,
scrollController: scrollController,
);
},
);
},
);
}
}

View File

@ -12,7 +12,6 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/utils/cookie.dart';
import 'package:pilipala/utils/global_data_cache.dart'; import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -68,7 +67,6 @@ class LoginUtils {
content = '${content + url}; \n'; content = '${content + url}; \n';
} }
try { try {
await SetCookie.onSet();
final result = await UserHttp.userInfo(); final result = await UserHttp.userInfo();
if (result['status'] && result['data'].isLogin) { if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功'); SmartDialog.showToast('登录成功');

View File

@ -137,7 +137,8 @@ class SettingBoxKey {
enableGradientBg = 'enableGradientBg', enableGradientBg = 'enableGradientBg',
enableDynamicSwitch = 'enableDynamicSwitch', enableDynamicSwitch = 'enableDynamicSwitch',
navBarSort = 'navBarSort', navBarSort = 'navBarSort',
actionTypeSort = 'actionTypeSort'; actionTypeSort = 'actionTypeSort',
enablePureBlack = 'enablePureBlack';
} }
class LocalCacheKey { class LocalCacheKey {