Merge branch 'main' into feature-minePage

This commit is contained in:
guozhigq
2024-10-16 14:17:24 +08:00
parent 676b2f18eb
commit 174eff7151
62 changed files with 1738 additions and 1371 deletions

View File

@ -295,7 +295,7 @@ class AboutController extends GetxController {
displayTime: const Duration(milliseconds: 500),
).then(
(value) => launchUrl(
Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),
Uri.parse('https://www.123684.com/s/9sVqVv-DEZ0A'),
mode: LaunchMode.externalApplication,
),
);
@ -349,7 +349,7 @@ class AboutController extends GetxController {
// 官网
webSiteUrl() {
launchUrl(
Uri.parse('https://pilipalanet.mysxl.cn'),
Uri.parse('https://pilipala.life'),
mode: LaunchMode.externalApplication,
);
}

View File

@ -189,8 +189,8 @@ class _BangumiInfoState extends State<BangumiInfo> {
Stack(
children: [
NetworkImgLayer(
width: 105,
height: 160,
width: 115,
height: 115 / 0.75,
src: widget.bangumiDetail!.cover!,
),
PBadge(
@ -208,7 +208,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
child: InkWell(
onTap: () => showIntroDetail(),
child: SizedBox(
height: 158,
height: 115 / 0.75,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,

View File

@ -96,7 +96,8 @@ class _BangumiPageState extends State<BangumiPage>
),
),
SizedBox(
height: 268,
height: Get.size.width / 3 / 0.75 +
MediaQuery.textScalerOf(context).scale(50.0),
child: FutureBuilder(
future: _futureBuilderFutureFollow,
builder:
@ -117,7 +118,6 @@ class _BangumiPageState extends State<BangumiPage>
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
left: StyleString.safeSpace,
right: index ==
@ -208,8 +208,8 @@ class _BangumiPageState extends State<BangumiPage>
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 +
MediaQuery.textScalerOf(context).scale(32.0),
mainAxisExtent: Get.size.width / 3 / 0.75 +
MediaQuery.textScalerOf(context).scale(42.0),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -86,9 +86,11 @@ class _BangumiPanelState extends State<BangumiPanel> {
item.aid,
item.cover,
);
if (_bottomSheetController != null) {
_bottomSheetController?.close();
}
try {
if (_bottomSheetController != null) {
_bottomSheetController?.close();
}
} catch (_) {}
currentIndex.value = i;
scrollToIndex();
}

View File

@ -37,7 +37,7 @@ class BangumiCardV extends StatelessWidget {
StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: 0.65,
aspectRatio: 0.75,
child: LayoutBuilder(builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;

View File

@ -8,7 +8,7 @@ import 'package:pilipala/utils/storage.dart';
import '../../http/index.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
bool flag = true;
late RxList tabs = [].obs;
RxInt initialIndex = 1.obs;
late TabController tabController;

View File

@ -64,7 +64,7 @@ class LiveRoomController extends GetxController {
? liveItem.pic
: (liveItem.cover != null && liveItem.cover != '')
? liveItem.cover
: null;
: '';
}
Request.getBuvid().then((value) => buvid = value);
}

View File

@ -108,6 +108,12 @@ class _LiveRoomPageState extends State<LiveRoomPage>
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final isPortrait = mediaQuery.orientation == Orientation.portrait;
final isLandscape = mediaQuery.orientation == Orientation.landscape;
final padding = mediaQuery.padding;
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -187,10 +193,8 @@ class _LiveRoomPageState extends State<LiveRoomPage>
children: [
Obx(
() => SizedBox(
height: MediaQuery.of(context).padding.top +
(_liveRoomController.isPortrait.value ||
MediaQuery.of(context).orientation ==
Orientation.landscape
height: padding.top +
(_liveRoomController.isPortrait.value || isLandscape
? 0
: kToolbarHeight),
),
@ -201,21 +205,18 @@ class _LiveRoomPageState extends State<LiveRoomPage>
if (plPlayerController.isFullScreen.value == true) {
plPlayerController.triggerFullScreen(status: false);
}
if (MediaQuery.of(context).orientation ==
Orientation.landscape) {
if (isLandscape) {
verticalScreen();
}
},
child: Obx(
() => Container(
width: Get.size.width,
height: MediaQuery.of(context).orientation ==
Orientation.landscape
height: isLandscape
? Get.size.height
: !_liveRoomController.isPortrait.value
? Get.size.width * 9 / 16
: Get.size.height -
MediaQuery.of(context).padding.top,
: Get.size.height - padding.top,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
@ -229,7 +230,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
// 定位 快速滑动到底部
Positioned(
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 80,
bottom: padding.bottom + 80,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 4),
@ -262,10 +263,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
titleSpacing: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
toolbarHeight:
MediaQuery.of(context).orientation == Orientation.portrait
? 56
: 0,
toolbarHeight: isPortrait ? 56 : 0,
title: FutureBuilder(
future: _futureBuilder,
builder: (context, snapshot) {
@ -317,35 +315,38 @@ class _LiveRoomPageState extends State<LiveRoomPage>
),
// 消息列表
Obx(
() => Positioned(
top: MediaQuery.of(context).padding.top +
kToolbarHeight +
(_liveRoomController.isPortrait.value
? Get.size.width
: Get.size.width * 9 / 16),
bottom: 90 + MediaQuery.of(context).padding.bottom,
left: 0,
right: 0,
child: buildMessageListUI(
context,
_liveRoomController,
_scrollController,
() => Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.only(
bottom: 90 + padding.bottom,
),
height: Get.size.height -
(padding.top +
kToolbarHeight +
(_liveRoomController.isPortrait.value
? Get.size.width
: Get.size.width * 9 / 16) +
100 +
padding.bottom),
child: buildMessageListUI(
context,
_liveRoomController,
_scrollController,
),
),
),
),
// 消息输入框
Visibility(
visible: MediaQuery.of(context).orientation == Orientation.portrait,
visible: isPortrait,
child: Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.only(
left: 14,
right: 14,
top: 4,
bottom: MediaQuery.of(context).padding.bottom + 20),
left: 14, right: 14, top: 4, bottom: padding.bottom + 20),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(20)),
@ -421,6 +422,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
],
),
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
@ -438,84 +440,82 @@ Widget buildMessageListUI(
LiveRoomController liveRoomController,
ScrollController scrollController,
) {
return Expanded(
child: Obx(
() => MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.5),
Colors.black,
],
stops: const [0.01, 0.05, 0.2],
).createShader(bounds);
return Obx(
() => MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.5),
Colors.black,
],
stops: const [0.01, 0.05, 0.2],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: GestureDetector(
onTap: () {
// 键盘失去焦点
FocusScope.of(context).requestFocus(FocusNode());
},
blendMode: BlendMode.dstIn,
child: GestureDetector(
onTap: () {
// 键盘失去焦点
FocusScope.of(context).requestFocus(FocusNode());
},
child: ListView.builder(
controller: scrollController,
itemCount: liveRoomController.messageList.length,
itemBuilder: (context, index) {
final LiveMessageModel liveMsgItem =
liveRoomController.messageList[index];
return Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: liveRoomController.isPortrait.value
? Colors.black.withOpacity(0.3)
: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
margin: EdgeInsets.only(
top: index == 0 ? 20.0 : 0.0,
bottom: 6.0,
left: 14.0,
right: 14.0,
),
padding: const EdgeInsets.symmetric(
vertical: 3.0,
horizontal: 10.0,
),
child: Text.rich(
TextSpan(
style: const TextStyle(color: Colors.white),
children: [
TextSpan(
text: '${liveMsgItem.userName}: ',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
print('Text clicked');
},
child: ListView.builder(
controller: scrollController,
itemCount: liveRoomController.messageList.length,
itemBuilder: (context, index) {
final LiveMessageModel liveMsgItem =
liveRoomController.messageList[index];
return Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: liveRoomController.isPortrait.value
? Colors.black.withOpacity(0.3)
: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
margin: EdgeInsets.only(
top: index == 0 ? 20.0 : 0.0,
bottom: 6.0,
left: 14.0,
right: 14.0,
),
padding: const EdgeInsets.symmetric(
vertical: 3.0,
horizontal: 10.0,
),
child: Text.rich(
TextSpan(
style: const TextStyle(color: Colors.white),
children: [
TextSpan(
text: '${liveMsgItem.userName}: ',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
),
TextSpan(
children: [
...buildMessageTextSpan(context, liveMsgItem)
],
// text: liveMsgItem.message,
),
],
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
print('Text clicked');
},
),
TextSpan(
children: [
...buildMessageTextSpan(context, liveMsgItem)
],
// text: liveMsgItem.message,
),
],
),
),
);
},
),
),
);
},
),
),
),

View File

@ -14,6 +14,7 @@ import '../../models/common/nav_bar_config.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[];
List<int> pagesIds = <int>[];
RxList navigationBars = [].obs;
late List defaultNavTabs;
late List<int> navBarSort;
@ -45,7 +46,8 @@ class MainController extends GetxController {
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
setNavBarConfig();
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
if (dynamicBadgeType.value != DynamicBadgeMode.hidden &&
pagesIds.contains(2)) {
getUnreadDynamic();
}
enableGradientBg =
@ -114,6 +116,7 @@ class MainController extends GetxController {
// 如果找不到匹配项默认索引设置为0或其他合适的值
selectedIndex = defaultIndex != -1 ? defaultIndex : 0;
pages = navigationBars.map<Widget>((e) => e['page']).toList();
pagesIds = navigationBars.map<int>((e) => e['id']).toList();
}
@override

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamic_badge_mode.dart';
@ -22,10 +23,10 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController());
final RankController _rankController = Get.put(RankController());
final DynamicsController _dynamicController = Get.put(DynamicsController());
final MineController _mineController = Get.put(MineController());
late HomeController _homeController;
RankController? _rankController;
late DynamicsController _dynamicController;
late MineController _mineController;
int? _lastSelectTime; //上次点击时间
Box setting = GStrorage.setting;
@ -38,6 +39,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
_mainController.pageController =
PageController(initialPage: _mainController.selectedIndex);
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
controllerInit();
}
void setIndex(int value) async {
@ -60,18 +62,18 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
}
if (currentPage is RankPage) {
if (_rankController.flag) {
if (_rankController!.flag) {
// 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_rankController.onRefresh();
_rankController!.onRefresh();
} else {
_rankController.animateToTop();
_rankController!.animateToTop();
}
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_rankController.flag = true;
_rankController!.flag = true;
} else {
_rankController.flag = false;
_rankController?.flag = false;
}
if (currentPage is DynamicsPage) {
@ -96,6 +98,18 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
}
}
void controllerInit() {
_homeController = Get.put(HomeController());
_dynamicController = Get.put(DynamicsController());
_mineController = Get.put(MineController());
if (_mainController.pagesIds.contains(1)) {
_rankController = Get.put(RankController());
}
if (_mainController.pagesIds.contains(2)) {
_dynamicController = Get.put(DynamicsController());
}
}
@override
void dispose() async {
await GStrorage.close();
@ -112,6 +126,14 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
MediaQuery.sizeOf(context).width * 9 / 16;
localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness:
Get.isDarkMode ? Brightness.light : Brightness.dark,
),
);
return PopScope(
canPop: false,
onPopInvoked: (bool didPop) async {

View File

@ -49,6 +49,8 @@ class MemberController extends GetxController {
if (res['status']) {
memberInfo.value = res['data'];
face.value = res['data'].face;
} else {
SmartDialog.showToast('用户信息请求异常:${res['msg']}');
}
return res;
}
@ -78,42 +80,10 @@ class MemberController extends GetxController {
return;
}
if (attribute.value == 128) {
blockUser();
return;
modifyRelation('block');
} else {
modifyRelation('follow');
}
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
await VideoHttp.relationMod(
mid: mid,
act: memberInfo.value.isFollowed! ? 2 : 1,
reSrc: 11,
);
memberInfo.value.isFollowed = !memberInfo.value.isFollowed!;
relationSearch();
SmartDialog.dismiss();
memberInfo.update((val) {});
},
child: const Text('确认'),
)
],
);
},
);
}
// 关系查询
@ -123,24 +93,15 @@ class MemberController extends GetxController {
var res = await UserHttp.hasFollow(mid);
if (res['status']) {
attribute.value = res['data']['attribute'];
switch (attribute.value) {
case 1:
attributeText.value = '悄悄关注';
break;
case 2:
attributeText.value = '已关注';
break;
case 6:
attributeText.value = '已互关';
break;
case 128:
attributeText.value = '已拉黑';
break;
default:
attributeText.value = '关注';
}
final Map<int, String> attributeTextMap = {
1: '悄悄关注',
2: '关注',
6: '已互关',
128: '已拉黑',
};
attributeText.value = attributeTextMap[attribute.value] ?? '关注';
if (res['data']['special'] == 1) {
attributeText.value += 'SP';
attributeText.value = '特别关注';
}
}
}
@ -151,16 +112,37 @@ class MemberController extends GetxController {
SmartDialog.showToast('账号未登录');
return;
}
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
modifyRelation('block');
}
// 合并关注/取关和拉黑逻辑
Future modifyRelation(String actionType) async {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
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!,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(attribute.value != 128 ? '确定拉黑UP主?' : '从黑名单移除UP主'),
content: Text(contentText),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
onPressed: () => Navigator.of(context).pop(),
child: Text(
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
@ -170,19 +152,26 @@ class MemberController extends GetxController {
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: mid,
act: attribute.value != 128 ? 5 : 6,
act: act,
reSrc: 11,
);
SmartDialog.dismiss();
if (res['status']) {
attribute.value = attribute.value != 128 ? 128 : 0;
attributeText.value = attribute.value == 128 ? '已拉黑' : '关注';
memberInfo.value.isFollowed = false;
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(''),
child: const Text(''),
)
],
);
@ -228,17 +217,14 @@ class MemberController extends GetxController {
// 跳转查看动态
void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid');
// 跳转查看投稿
void pushArchivesPage() => Get.toNamed('/memberArchive?mid=$mid');
// 跳转查看专栏
void pushSeasonsPage() {}
// 跳转查看最近投币
void pushRecentCoinsPage() async {
if (recentCoinsList.isNotEmpty) {}
}
// 跳转查看收藏夹
void pushfavPage() => Get.toNamed('/fav?mid=$mid');
// 跳转图文专栏
void pushArticlePage() => Get.toNamed('/memberArticle?mid=$mid');

View File

@ -1,13 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'widgets/commen_widget.dart';
import 'widgets/conis.dart';
import 'widgets/like.dart';
import 'widgets/profile.dart';
@ -65,259 +65,233 @@ class _MemberPageState extends State<MemberPage>
@override
Widget build(BuildContext context) {
return Scaffold(
primary: true,
body: Column(
children: [
AppBar(
title: StreamBuilder(
stream: appbarStream.stream.distinct(),
initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
),
const SizedBox(width: 10),
Obx(
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
fontSize: 14),
),
),
],
)
],
appBar: AppBar(
title: StreamBuilder(
stream: appbarStream.stream.distinct(),
initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid != _memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
)
],
PopupMenuItem(
onTap: () => _memberController.shareUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid != _memberController.mid
? '分享UP主'
: '分享我的主页'),
],
const SizedBox(width: 10),
Obx(
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 14),
),
),
],
),
const SizedBox(width: 4),
],
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
Expanded(
child: SingleChildScrollView(
controller: _extendNestCtr,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
),
child: Column(
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid != _memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
)
],
PopupMenuItem(
onTap: () => _memberController.shareUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
profileWidget(),
/// 动态链接
Obx(
() => ListTile(
onTap: _memberController.pushDynamicsPage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的动态'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 视频
Obx(
() => ListTile(
onTap: _memberController.pushArchivesPage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的投稿'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 他的收藏夹
Obx(
() => ListTile(
onTap: _memberController.pushfavPage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的收藏'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 专栏
Obx(
() => ListTile(
onTap: _memberController.pushArticlePage,
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的专栏'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 合集
Obx(
() => ListTile(
title: Text(
'${_memberController.isOwner.value ? '' : 'Ta'}的合集')),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: FutureBuilder(
future: _memberSeasonsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
if (data['data'].seasonsList.isEmpty) {
return commenWidget('用户没有设置合集');
} else {
return MemberSeasonsPanel(data: data['data']);
}
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
/// 追番
/// 最近投币
Obx(
() => _memberController.recentCoinsList.isNotEmpty
? const ListTile(title: Text('最近投币的视频'))
: const SizedBox(),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberCoinsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberCoinsPanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
/// 最近点赞
Obx(
() => _memberController.recentLikeList.isNotEmpty
? const ListTile(title: Text('最近点赞的视频'))
: const SizedBox(),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberLikeFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberLikePanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
const Icon(Icons.share_outlined, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid != _memberController.mid
? '分享UP主'
: '分享我的主页'),
],
),
),
],
),
const SizedBox(width: 4),
],
),
primary: true,
body: ListView(
controller: _extendNestCtr,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
),
children: [
profileWidget(),
/// 动态链接
Obx(
() => ListTile(
onTap: _memberController.pushDynamicsPage,
title: Text('${_memberController.isOwner.value ? '' : 'Ta'}的动态'),
trailing: const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 视频
Obx(
() => ListTile(
onTap: _memberController.pushArchivesPage,
title: Text('${_memberController.isOwner.value ? '' : 'Ta'}的投稿'),
trailing: const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 他的收藏夹
Obx(
() => ListTile(
onTap: _memberController.pushfavPage,
title: Text('${_memberController.isOwner.value ? '' : 'Ta'}的收藏'),
trailing: const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 专栏
Obx(
() => ListTile(
onTap: _memberController.pushArticlePage,
title: Text('${_memberController.isOwner.value ? '' : 'Ta'}的专栏'),
trailing: const Icon(Icons.arrow_forward_outlined, size: 19),
),
),
/// 合集
Obx(
() => ListTile(
title: Text('${_memberController.isOwner.value ? '' : 'Ta'}的合集'),
),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: FutureBuilder(
future: _memberSeasonsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
if (data['data'].seasonsList.isEmpty) {
return const CommenWidget(msg: '用户没有设置合集');
} else {
return MemberSeasonsPanel(data: data['data']);
}
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
/// 追番
/// 最近投币
Obx(
() => _memberController.recentCoinsList.isNotEmpty
? const ListTile(title: Text('最近投币的视频'))
: const SizedBox(),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberCoinsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberCoinsPanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
/// 最近点赞
Obx(
() => _memberController.recentLikeList.isNotEmpty
? const ListTile(title: Text('最近点赞的视频'))
: const SizedBox(),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberLikeFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberLikePanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
],
@ -334,115 +308,90 @@ class _MemberPageState extends State<MemberPage>
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
Rx<MemberInfoModel> memberInfo = _memberController.memberInfo;
return Obx(
() => Stack(
alignment: AlignmentDirectional.center,
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
ProfilePanel(ctr: _memberController),
const SizedBox(height: 20),
Row(
children: [
ProfilePanel(ctr: _memberController),
const SizedBox(height: 20),
Row(
children: [
Flexible(
child: Text(
_memberController.memberInfo.value.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontWeight: FontWeight.bold,
color: _memberController.memberInfo.value
.vip!.nicknameColor !=
null
? Color(_memberController.memberInfo
.value.vip!.nicknameColor!)
: null),
)),
const SizedBox(width: 2),
if (_memberController.memberInfo.value.sex == '')
const Icon(
FontAwesomeIcons.venus,
size: 14,
color: Colors.pink,
),
if (_memberController.memberInfo.value.sex == '')
const Icon(
FontAwesomeIcons.mars,
size: 14,
color: Colors.blue,
),
const SizedBox(width: 4),
Image.asset(
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
height: 11,
),
const SizedBox(width: 6),
if (_memberController
.memberInfo.value.vip!.status ==
1 &&
_memberController.memberInfo.value.vip!
.label!['img_label_uri_hans'] !=
'') ...[
Image.network(
_memberController.memberInfo.value.vip!
.label!['img_label_uri_hans'],
height: 20,
),
] else if (_memberController
.memberInfo.value.vip!.status ==
1 &&
_memberController.memberInfo.value.vip!
.label!['img_label_uri_hans_static'] !=
'') ...[
Image.network(
_memberController.memberInfo.value.vip!
.label!['img_label_uri_hans_static'],
height: 20,
),
]
],
Flexible(
child: Text(
memberInfo.value.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontWeight: FontWeight.bold,
color: memberInfo.value.vip!.nicknameColor !=
null
? Color(_memberController
.memberInfo.value.vip!.nicknameColor!)
: null),
)),
const SizedBox(width: 2),
if (memberInfo.value.sex == '')
const Icon(
FontAwesomeIcons.venus,
size: 14,
color: Colors.pink,
),
if (memberInfo.value.sex == '')
const Icon(
FontAwesomeIcons.mars,
size: 14,
color: Colors.blue,
),
const SizedBox(width: 4),
Image.asset(
'assets/images/lv/lv${memberInfo.value.level}.png',
height: 11,
),
if (_memberController
.memberInfo.value.official!['title'] !=
'') ...[
const SizedBox(height: 6),
Text.rich(
maxLines: 2,
TextSpan(
text: _memberController
.memberInfo.value.official!['role'] ==
1
? '个人认证:'
: '企业认证:',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
children: [
TextSpan(
text: _memberController
.memberInfo.value.official!['title'],
),
],
),
softWrap: true,
const SizedBox(width: 6),
if (memberInfo.value.vip!.status == 1 &&
memberInfo
.value.vip!.label!['img_label_uri_hans'] !=
'') ...[
Image.network(
memberInfo.value.vip!.label!['img_label_uri_hans'],
height: 20,
),
],
const SizedBox(height: 6),
if (_memberController.memberInfo.value.sign != '')
SelectableText(
_memberController.memberInfo.value.sign!,
] else if (memberInfo.value.vip!.status == 1 &&
memberInfo.value.vip!
.label!['img_label_uri_hans_static'] !=
'') ...[
Image.network(
memberInfo
.value.vip!.label!['img_label_uri_hans_static'],
height: 20,
),
]
],
),
if (memberInfo.value.official!['title'] != '') ...[
const SizedBox(height: 6),
Text(
memberInfo.value.official!['role'] == 1
? '个人认证:${memberInfo.value.official!['title']}'
: '企业认证:${memberInfo.value.official!['title']}',
maxLines: 2,
softWrap: true,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
],
const SizedBox(height: 6),
SelectableText(memberInfo.value.sign ?? ''),
],
),
);
} else {
return const SizedBox();
return ProfilePanel(ctr: _memberController, loadingStatus: true);
}
} else {
// 骨架屏
@ -452,22 +401,4 @@ class _MemberPageState extends State<MemberPage>
),
);
}
Widget commenWidget(msg) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 30,
),
child: Center(
child: Text(
msg,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
class CommenWidget extends StatelessWidget {
final String msg;
const CommenWidget({required this.msg, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 10.0),
child: Center(
child: Text(
msg,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@ -17,253 +17,245 @@ class ProfilePanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
MemberInfoModel memberInfo = ctr.memberInfo.value;
return Builder(
builder: ((context) {
return Padding(
padding:
EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
child: Row(
children: [
Hero(
tag: ctr.heroTag!,
child: Stack(
children: [
NetworkImgLayer(
width: 90,
height: 90,
type: 'avatar',
src: !loadingStatus ? memberInfo.face : ctr.face.value,
),
if (!loadingStatus &&
memberInfo.liveRoom != null &&
memberInfo.liveRoom!.liveStatus == 1)
Positioned(
bottom: 0,
left: 14,
child: GestureDetector(
onTap: () {
LiveItemModel liveItem = LiveItemModel.fromJson({
'title': memberInfo.liveRoom!.title,
'uname': memberInfo.name,
'face': memberInfo.face,
'roomid': memberInfo.liveRoom!.roomId,
'watched_show': memberInfo.liveRoom!.watchedShow,
});
Get.toNamed(
'/liveRoom?roomid=${memberInfo.liveRoom!.roomId}',
arguments: {'liveItem': liveItem},
);
},
child: Container(
padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(10)),
),
child: Row(children: [
Image.asset(
'assets/images/live.gif',
height: 10,
),
Text(
' 直播中',
style: TextStyle(
color: Colors.white,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
)
]),
),
),
)
],
final int? mid = memberInfo.mid;
final String? name = memberInfo.name;
Map<String, dynamic> buildStatItem({
required String label,
required String value,
required VoidCallback onTap,
}) {
return {
'label': label,
'value': value,
'fn': onTap,
};
}
final List<Map<String, dynamic>> statList = [
buildStatItem(
label: '关注',
value: !loadingStatus ? "${ctr.userStat!['following']}" : '-',
onTap: () {
Get.toNamed('/follow?mid=$mid&name=$name');
},
),
buildStatItem(
label: '粉丝',
value: !loadingStatus
? ctr.userStat!['follower'] != null
? Utils.numFormat(ctr.userStat!['follower'])
: '-'
: '-',
onTap: () {
Get.toNamed('/fan?mid=$mid&name=$name');
},
),
buildStatItem(
label: '获赞',
value: !loadingStatus
? ctr.userStat!['likes'] != null
? Utils.numFormat(ctr.userStat!['likes'])
: '-'
: '-',
onTap: () {},
),
];
return Padding(
padding: const EdgeInsets.only(top: 30, left: 4),
child: Row(
children: [
Hero(
tag: ctr.heroTag!,
child: Stack(
children: [
NetworkImgLayer(
width: 90,
height: 90,
type: 'avatar',
src: !loadingStatus ? memberInfo.face : ctr.face.value,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.only(top: 10, left: 10, right: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
InkWell(
onTap: () {
Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
},
child: Column(
children: [
Text(
!loadingStatus
? ctr.userStat!['following'].toString()
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold),
),
Text(
'关注',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
if (!loadingStatus &&
memberInfo.liveRoom != null &&
memberInfo.liveRoom!.liveStatus == 1)
Positioned(
bottom: 0,
left: 14,
child: GestureDetector(
onTap: () {
LiveItemModel liveItem = LiveItemModel(
title: memberInfo.liveRoom!.title,
uname: memberInfo.name,
face: memberInfo.face,
roomId: memberInfo.liveRoom!.roomId,
watchedShow: memberInfo.liveRoom!.watchedShow,
);
Get.toNamed(
'/liveRoom?roomid=${memberInfo.liveRoom!.roomId}',
arguments: {'liveItem': liveItem},
);
},
child: Container(
padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(10)),
),
child: Row(children: [
Image.asset(
'assets/images/live.gif',
height: 10,
),
InkWell(
onTap: () {
Get.toNamed(
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
},
child: Column(
children: [
Text(
!loadingStatus
? ctr.userStat!['follower'] != null
? Utils.numFormat(
ctr.userStat!['follower'],
)
: '-'
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold)),
Text(
'粉丝',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
),
Column(
children: [
Text(
!loadingStatus
? ctr.userStat!['likes'] != null
? Utils.numFormat(
ctr.userStat!['likes'],
)
: '-'
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold)),
Text(
'获赞',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
],
Text(
' 直播中',
style: TextStyle(
color: Colors.white,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
)
]),
),
),
const SizedBox(height: 10),
if (ctr.ownerMid != ctr.mid && ctr.ownerMid != -1) ...[
Row(
children: [
Obx(
() => Expanded(
child: TextButton(
onPressed: () => loadingStatus
? null
: ctr.actionRelationMod(),
style: TextButton.styleFrom(
foregroundColor: ctr.attribute.value == -1
? Colors.transparent
: ctr.attribute.value != 0
? Theme.of(context)
.colorScheme
.outline
: Theme.of(context)
.colorScheme
.onPrimary,
backgroundColor: ctr.attribute.value != 0
? Theme.of(context)
.colorScheme
.onInverseSurface
: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
child: Obx(() => Text(ctr.attributeText.value)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextButton(
onPressed: () {
Get.toNamed(
'/whisperDetail',
parameters: {
'name': memberInfo.name!,
'face': memberInfo.face!,
'mid': memberInfo.mid.toString(),
'heroTag': ctr.heroTag!,
},
);
},
style: TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.onInverseSurface,
),
child: const Text('发消息'),
),
)
],
)
],
if (ctr.ownerMid == ctr.mid && ctr.ownerMid != -1) ...[
TextButton(
onPressed: () {
Get.toNamed('/mineEdit');
},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 80, right: 80),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary,
),
child: const Text('编辑资料'),
)
],
if (ctr.ownerMid == -1) ...[
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 80, right: 80),
foregroundColor:
Theme.of(context).colorScheme.outline,
backgroundColor:
Theme.of(context).colorScheme.onInverseSurface,
),
child: const Text('未登录'),
)
]
],
),
),
],
)
],
),
),
);
}),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: statList.map((item) {
return buildStatColumn(
context,
item['label'],
item['value'],
item['fn'],
);
}).toList(),
),
),
const SizedBox(height: 16),
if (ctr.ownerMid != ctr.mid && ctr.ownerMid != -1)
buildActionButtons(context, ctr, memberInfo),
if (ctr.ownerMid == ctr.mid && ctr.ownerMid != -1)
buildEditProfileButton(context),
if (ctr.ownerMid == -1) buildNotLoggedInButton(context),
],
),
),
],
),
);
}
Widget buildStatColumn(
BuildContext context,
String label,
String value,
VoidCallback? onTap,
) {
return InkWell(
onTap: onTap,
child: Column(
children: [
Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
Text(
label,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
),
),
],
),
);
}
Widget buildActionButtons(
BuildContext context,
dynamic ctr,
MemberInfoModel memberInfo,
) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
const SizedBox(width: 20),
Obx(
() => Expanded(
child: TextButton(
onPressed: () => loadingStatus ? null : ctr.actionRelationMod(),
style: TextButton.styleFrom(
foregroundColor: ctr.attribute.value == -1
? Colors.transparent
: ctr.attribute.value != 0
? colorScheme.outline
: colorScheme.onPrimary,
backgroundColor: ctr.attribute.value != 0
? colorScheme.onInverseSurface
: colorScheme.primary,
),
child: Obx(() => Text(ctr.attributeText.value)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextButton(
onPressed: () {
Get.toNamed(
'/whisperDetail',
parameters: {
'name': memberInfo.name!,
'face': memberInfo.face!,
'mid': memberInfo.mid.toString(),
'heroTag': ctr.heroTag!,
},
);
},
style: TextButton.styleFrom(
backgroundColor: colorScheme.onInverseSurface,
),
child: const Text('发消息'),
),
),
],
);
}
Widget buildEditProfileButton(BuildContext context) {
return TextButton(
onPressed: () {
Get.toNamed('/mineEdit');
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 80),
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
child: const Text('编辑资料'),
);
}
Widget buildNotLoggedInButton(BuildContext context) {
return TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 80),
foregroundColor: Theme.of(context).colorScheme.outline,
backgroundColor: Theme.of(context).colorScheme.onInverseSurface,
),
child: const Text('未登录'),
);
}
}

View File

@ -10,7 +10,6 @@ class MemberArticleController extends GetxController {
int pn = 1;
String? offset;
bool hasMore = true;
String? wWebid;
RxBool isLoading = false.obs;
RxList<MemberArticleItemModel> articleList = <MemberArticleItemModel>[].obs;
@ -20,25 +19,11 @@ class MemberArticleController extends GetxController {
mid = int.parse(Get.parameters['mid']!);
}
// 获取wWebid
Future getWWebid() async {
var res = await MemberHttp.getWWebid(mid: mid);
if (res['status']) {
wWebid = res['data'];
} else {
wWebid = '-1';
SmartDialog.showToast(res['msg']);
}
}
Future getMemberArticle(type) async {
if (isLoading.value) {
return;
}
isLoading.value = true;
if (wWebid == null) {
await getWWebid();
}
if (type == 'init') {
pn = 1;
articleList.clear();
@ -47,7 +32,6 @@ class MemberArticleController extends GetxController {
mid: mid,
pn: pn,
offset: offset,
wWebid: wWebid!,
);
if (res['status']) {
offset = res['data'].offset;

View File

@ -125,21 +125,29 @@ class LikeItem extends StatelessWidget {
Color outline = Theme.of(context).colorScheme.outline;
final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();
int usersLen = item.users!.length > 3 ? 3 : item.users!.length;
final String bvid = item.item!.uri!.split('/').last;
final Uri uri = Uri.parse(item.item!.uri!);
final String path = uri.path;
final String bvid = path.split('/').last;
/// bilibili://
final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
final Map<String, String> queryParameters = nativeUri.queryParameters;
final String type = item.item!.type!;
// cid
final String? argCid = queryParameters['cid'];
// 页码
final String page =
item.item!.nativeUri!.split('page=').last.split('&').first;
final String? page = queryParameters['page'];
// 根评论id
final String commentRootId =
item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;
final String? commentRootId = queryParameters['comment_root_id'];
// 二级评论id
final String commentSecondaryId =
item.item!.nativeUri!.split('comment_secondary_id=').last;
final String? commentSecondaryId = queryParameters['comment_secondary_id'];
return InkWell(
onTap: () async {
try {
final int cid = await SearchHttp.ab2c(bvid: bvid);
final int cid = argCid != null
? int.parse(argCid)
: await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
@ -148,8 +156,8 @@ class LikeItem extends StatelessWidget {
'heroTag': heroTag,
},
);
} catch (_) {
SmartDialog.showToast('视频可能失效了');
} catch (e) {
SmartDialog.showToast('视频可能失效了$e');
}
},
child: Stack(
@ -222,7 +230,7 @@ class LikeItem extends StatelessWidget {
),
),
const SizedBox(width: 25),
if (item.item!.type! == 'reply')
if (type == 'reply' || type == 'danmu')
Container(
width: 60,
height: 60,
@ -234,7 +242,7 @@ class LikeItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
if (item.item!.type! == 'video')
if (type == 'video')
NetworkImgLayer(
width: 60,
height: 60,

View File

@ -157,7 +157,7 @@ class _OpusPageState extends State<OpusPage> {
Container(
alignment: TextHelper.getAlignment(paragraph.align),
margin: const EdgeInsets.only(bottom: 10),
child: Text.rich(
child: SelectableText.rich(
TextSpan(
children: paragraph.text?.nodes?.map((node) {
return TextHelper.buildTextSpan(

View File

@ -20,7 +20,7 @@ class ReadPageController extends GetxController {
super.onInit();
title.value = Get.parameters['title'] ?? '';
id = Get.parameters['id']!;
articleType = Get.parameters['articleType']!;
articleType = Get.parameters['articleType'] ?? 'read';
url = 'https://www.bilibili.com/read/cv$id';
scrollController.addListener(_scrollListener);
fetchViewInfo();

View File

@ -126,7 +126,6 @@ class _ReadPageState extends State<ReadPage> {
Widget _buildContent(ReadDataModel cvData) {
final List<String> picList = _extractPicList(cvData);
final List<String> imgList = extractDataSrc(cvData.readInfo!.content!);
return Padding(
padding: EdgeInsets.fromLTRB(
16, 0, 16, MediaQuery.of(context).padding.bottom + 40),
@ -163,9 +162,11 @@ class _ReadPageState extends State<ReadPage> {
padding: const EdgeInsets.only(bottom: 20),
child: _buildAuthorWidget(cvData),
),
HtmlRender(
htmlContent: cvData.readInfo!.content!,
imgList: imgList,
SelectionArea(
child: HtmlRender(
htmlContent: cvData.readInfo!.content!,
imgList: imgList,
),
),
],
);
@ -206,7 +207,7 @@ class _ReadPageState extends State<ReadPage> {
return Container(
alignment: TextHelper.getAlignment(paragraph.align),
margin: const EdgeInsets.only(bottom: 10),
child: Text.rich(
child: SelectableText.rich(
TextSpan(
children: paragraph.text?.nodes?.map((node) {
return TextHelper.buildTextSpan(node, paragraph.align, context);

View File

@ -24,7 +24,9 @@ class SearchPanelController extends GetxController {
searchType: searchType!,
keyword: keyword!,
page: page.value,
order: searchType!.type != 'video' ? null : order.value,
order: !['video', 'article'].contains(searchType!.type)
? null
: (order.value == '' ? null : order.value),
duration: searchType!.type != 'video' ? null : duration.value,
tids: searchType!.type != 'video' ? null : tids.value,
);

View File

@ -1,3 +1,5 @@
// ignore_for_file: invalid_use_of_protected_member
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -45,6 +47,11 @@ class _SearchPanelState extends State<SearchPanel>
),
tag: widget.searchType!.type + widget.keyword!,
);
/// 专栏默认排序
if (widget.searchType == SearchType.article) {
_searchPanelController.order.value = 'totalrank';
}
scrollController = _searchPanelController.scrollController;
scrollController.addListener(() async {
if (scrollController.position.pixels >=
@ -84,7 +91,6 @@ class _SearchPanelState extends State<SearchPanel>
case SearchType.video:
return SearchVideoPanel(
ctr: _searchPanelController,
// ignore: invalid_use_of_protected_member
list: list.value,
);
case SearchType.media_bangumi:
@ -94,7 +100,10 @@ class _SearchPanelState extends State<SearchPanel>
case SearchType.live_room:
return searchLivePanel(context, ctr, list);
case SearchType.article:
return searchArticlePanel(context, ctr, list);
return SearchArticlePanel(
ctr: _searchPanelController,
list: list.value,
);
default:
return const SizedBox();
}

View File

@ -1,106 +1,249 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/search_panel/index.dart';
import 'package:pilipala/utils/utils.dart';
class SearchArticlePanel extends StatelessWidget {
SearchArticlePanel({
required this.ctr,
this.list,
Key? key,
}) : super(key: key);
final SearchPanelController ctr;
final List? list;
final ArticlePanelController controller = Get.put(ArticlePanelController());
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.topCenter,
children: [
searchArticlePanel(context, ctr, list),
Container(
width: double.infinity,
height: 36,
padding: const EdgeInsets.only(left: 8, top: 0, right: 8),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Obx(
() => Wrap(
// spacing: ,
children: [
for (var i in controller.filterList) ...[
CustomFilterChip(
label: i['label'],
type: i['type'],
selectedType: controller.selectedType.value,
callFn: (bool selected) async {
controller.selectedType.value = i['type'];
ctr.order.value =
i['type'].toString().split('.').last;
SmartDialog.showLoading(msg: 'loading');
await ctr.onRefresh();
SmartDialog.dismiss();
},
),
]
],
),
),
),
),
],
),
),
],
);
}
}
Widget searchArticlePanel(BuildContext context, ctr, list) {
TextStyle textStyle = TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline);
return ListView.builder(
controller: ctr!.scrollController,
itemCount: list.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Get.toNamed('/read', parameters: {
'title': list[index].subTitle,
'id': list[index].id.toString(),
'articleType': 'read'
});
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (list[index].imageUrls != null &&
list[index].imageUrls.isNotEmpty)
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return NetworkImgLayer(
width: maxWidth,
height: maxHeight,
src: list[index].imageUrls.first,
);
}),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
return Padding(
padding: const EdgeInsets.only(top: 36),
child: list!.isNotEmpty
? ListView.builder(
controller: ctr!.scrollController,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: list.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Get.toNamed('/read', parameters: {
'title': list[index].subTitle,
'id': list[index].id.toString(),
'articleType': 'read'
});
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
maxLines: 2,
text: TextSpan(
children: [
for (var i in list[index].title) ...[
TextSpan(
text: i['text'],
style: TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
color: i['type'] == 'em'
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface,
children: <Widget>[
if (list[index].imageUrls != null &&
list[index].imageUrls.isNotEmpty)
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return NetworkImgLayer(
width: maxWidth,
height: maxHeight,
src: list[index].imageUrls.first,
);
}),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
maxLines: 2,
text: TextSpan(
children: [
for (var i in list[index].title) ...[
TextSpan(
text: i['text'],
style: TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
color: i['type'] == 'em'
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface,
),
),
]
],
),
),
]
],
const Spacer(),
Text(
Utils.dateFormat(list[index].pubTime,
formatType: 'detail'),
style: textStyle),
Row(
children: [
Text('${list[index].view}浏览',
style: textStyle),
Text('', style: textStyle),
Text('${list[index].reply}评论',
style: textStyle),
],
),
],
),
),
),
const Spacer(),
Text(
Utils.dateFormat(list[index].pubTime,
formatType: 'detail'),
style: textStyle),
Row(
children: [
Text('${list[index].view}浏览', style: textStyle),
Text('', style: textStyle),
Text('${list[index].reply}评论', style: textStyle),
],
),
],
),
),
),
],
),
);
}),
),
);
},
);
}),
),
);
},
)
: CustomScrollView(
slivers: [
HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
)
],
),
);
}
class CustomFilterChip extends StatelessWidget {
const CustomFilterChip({
this.label,
this.type,
this.selectedType,
this.callFn,
Key? key,
}) : super(key: key);
final String? label;
final ArticleFilterType? type;
final ArticleFilterType? selectedType;
final Function? callFn;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 34,
child: FilterChip(
padding: const EdgeInsets.only(left: 11, right: 11),
labelPadding: EdgeInsets.zero,
label: Text(
label!,
style: const TextStyle(fontSize: 13),
),
labelStyle: TextStyle(
color: type == selectedType
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline),
selected: type == selectedType,
showCheckmark: false,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
selectedColor: Colors.transparent,
// backgroundColor:
// Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
backgroundColor: Colors.transparent,
side: BorderSide.none,
onSelected: (bool selected) => callFn!(selected),
),
);
}
}
class ArticlePanelController extends GetxController {
RxList<Map> filterList = [{}].obs;
Rx<ArticleFilterType> selectedType = ArticleFilterType.values.first.obs;
@override
void onInit() {
List<Map<String, dynamic>> list = ArticleFilterType.values
.map((type) => {
'label': type.description,
'type': type,
})
.toList();
filterList.value = list;
super.onInit();
}
}

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/utils/global_data_cache.dart';
@ -22,7 +23,7 @@ class _PlayGesturePageState extends State<PlayGesturePage> {
void initState() {
super.initState();
fullScreenGestureMode = setting.get(SettingBoxKey.fullScreenGestureMode,
defaultValue: FullScreenGestureMode.values.last.index);
defaultValue: FullScreenGestureMode.fromBottomtoTop.index);
}
@override
@ -71,6 +72,7 @@ class _PlayGesturePageState extends State<PlayGesturePage> {
GlobalDataCache().fullScreenGestureMode.index;
setting.put(
SettingBoxKey.fullScreenGestureMode, fullScreenGestureMode);
SmartDialog.showToast('设置成功');
setState(() {});
}
},

View File

@ -675,7 +675,6 @@ class VideoDetailController extends GetxController
@override
void onClose() {
super.onClose();
plPlayerController.dispose();
tabCtr.removeListener(() {
onTabChanged();
});

View File

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
@ -154,11 +153,10 @@ class VideoIntroController extends GetxController {
}
if (hasLike.value && hasCoin.value && hasFav.value) {
// 已点赞、投币、收藏
SmartDialog.showToast('🙏 UP已经收到了');
SmartDialog.showToast('UP已经收到了');
return false;
}
var result = await VideoHttp.oneThree(bvid: bvid);
print('🤣🦴:${result["data"]}');
if (result['status']) {
hasLike.value = result["data"]["like"];
hasCoin.value = result["data"]["coin"];
@ -413,7 +411,12 @@ class VideoIntroController extends GetxController {
}
// 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid, cover) async {
Future changeSeasonOrbangu(
String bvid,
int cid,
int? aid,
String? cover,
) async {
// 重新获取视频资源
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
@ -424,13 +427,14 @@ class VideoIntroController extends GetxController {
releatedCtr.queryRelatedVideo();
}
videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid);
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl();
videoDetailCtr.clearSubtitleContent();
videoDetailCtr
..bvid = bvid
..oid.value = aid ?? IdUtils.bv2av(bvid)
..cid.value = cid
..danmakuCid.value = cid
..cover.value = cover ?? ''
..queryVideoUrl()
..clearSubtitleContent();
await videoDetailCtr.getSubtitle();
videoDetailCtr.setSubtitleContent();
// 重新请求评论
@ -480,7 +484,13 @@ class VideoIntroController extends GetxController {
final List episodes = [];
bool isPages = false;
late String cover;
if (videoDetail.value.ugcSeason != null) {
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
/// 优先稍后再看、收藏夹
if (videoDetailCtr.isWatchLaterVisible.value) {
episodes.addAll(videoDetailCtr.mediaList);
} else if (videoDetail.value.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
@ -497,10 +507,15 @@ class VideoIntroController extends GetxController {
episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1;
cover = episodes[nextIndex].cover;
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
int cid = episodes[nextIndex].cid!;
while (cid == -1) {
nextIndex += 1;
SmartDialog.showToast('当前视频暂不支持播放,自动跳过');
cid = episodes[nextIndex].cid!;
}
// 列表循环
if (nextIndex >= episodes.length) {
if (platRepeat == PlayRepeat.listCycle) {
@ -510,7 +525,6 @@ class VideoIntroController extends GetxController {
return;
}
}
final int cid = episodes[nextIndex].cid!;
final String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid, cover);
@ -604,4 +618,34 @@ class VideoIntroController extends GetxController {
).buildShowContent(Get.context!),
);
}
//
oneThreeDialog() {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('是否一键三连'),
actions: [
TextButton(
onPressed: () => navigator!.pop(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(Get.context!).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
actionOneThree();
navigator!.pop();
},
child: const Text('确认'),
)
],
);
},
);
}
}

View File

@ -1,7 +1,4 @@
import 'dart:ffi';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -151,11 +148,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
RxBool isExpand = false.obs;
late ExpandableController _expandableCtr;
// 一键三连动画
late AnimationController _controller;
late Animation<double> _scaleTransition;
final RxDouble _progress = 0.0.obs;
void Function()? handleState(Future<dynamic> Function() action) {
return isProcessing
? null
@ -178,26 +170,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
owner = widget.videoDetail!.owner;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
/// 一键三连动画
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
reverseDuration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleTransition = Tween<double>(begin: 0.5, end: 1.5).animate(_controller)
..addListener(() async {
_progress.value =
double.parse((_scaleTransition.value - 0.5).toStringAsFixed(3));
if (_progress.value == 1) {
if (_controller.status == AnimationStatus.completed) {
await videoIntroController.actionOneThree();
}
_progress.value = 0;
_scaleTransition.removeListener(() {});
_controller.stop();
}
});
}
// 收藏
@ -279,8 +251,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
@override
void dispose() {
_expandableCtr.dispose();
_controller.dispose();
_scaleTransition.removeListener(() {});
super.dispose();
}
@ -573,131 +543,34 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionGrid(BuildContext context, videoIntroController) {
final actionTypeSort = GlobalDataCache().actionTypeSort;
Widget progressWidget(progress) {
return SizedBox(
width: const IconThemeData.fallback().size! + 5,
height: const IconThemeData.fallback().size! + 5,
child: CircularProgressIndicator(
value: progress.value,
strokeWidth: 2,
),
);
}
Map<String, Widget> menuListWidgets = {
'like': Obx(
() {
bool likeStatus = videoIntroController.hasLike.value;
ColorScheme colorScheme = Theme.of(context).colorScheme;
return Stack(
children: [
Positioned(
top: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size!),
left: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size! + 5) / 2,
child: progressWidget(_progress)),
InkWell(
onTapDown: (details) {
feedBack();
if (videoIntroController.userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
_controller.forward();
},
onTapUp: (TapUpDetails details) {
if (_progress.value == 0) {
feedBack();
EasyThrottle.throttle(
'my-throttler', const Duration(milliseconds: 200), () {
videoIntroController.actionLikeVideo();
});
}
_controller.reverse();
},
onTapCancel: () {
_controller.reverse();
},
borderRadius: StyleString.mdRadius,
child: SizedBox(
width: (Get.size.width - 24) / 5,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 4),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Icon(
key: ValueKey<bool>(likeStatus),
likeStatus
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
color: likeStatus
? colorScheme.primary
: colorScheme.outline,
size: 21,
),
),
const SizedBox(height: 6),
Text(
widget.videoDetail!.stat!.like!.toString(),
style: TextStyle(
color: likeStatus ? colorScheme.primary : null,
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize,
),
)
],
),
),
),
],
);
},
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo),
onLongPress: () => videoIntroController.oneThreeDialog(),
selectStatus: videoIntroController.hasLike.value,
text: widget.videoDetail!.stat!.like!.toString(),
),
),
'coin': Obx(
() => Stack(
children: [
Positioned(
top: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size!),
left: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size! + 5) / 2,
child: progressWidget(_progress)),
ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
text: widget.videoDetail!.stat!.coin!.toString(),
),
],
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
text: widget.videoDetail!.stat!.coin!.toString(),
),
),
'collect': Obx(
() => Stack(
children: [
Positioned(
top: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size!),
left: ((Get.size.width - 24) / 5) / 2 -
(const IconThemeData.fallback().size! + 5) / 2,
child: progressWidget(_progress)),
ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
text: widget.videoDetail!.stat!.favorite!.toString(),
),
],
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
text: widget.videoDetail!.stat!.favorite!.toString(),
),
),
'watchLater': ActionItem(

View File

@ -153,7 +153,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
child: Container(
height: 40,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
color: Theme.of(context).colorScheme.surface,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.surface,
blurRadius: 0.0,
spreadRadius: 0.0,
offset: const Offset(2, 0),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@ -9,6 +9,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/main/index.dart';
@ -18,6 +19,7 @@ import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart';
@ -48,6 +50,8 @@ class ReplyItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool isOwner = int.parse(replyItem!.member!.mid!) ==
(GlobalDataCache().userInfo?.mid ?? -1);
return Material(
child: InkWell(
// 点击整个评论区 评论详情/回复
@ -73,6 +77,7 @@ class ReplyItem extends StatelessWidget {
return MorePanel(
item: replyItem,
mainFloor: true,
isOwner: isOwner,
);
},
);
@ -195,25 +200,36 @@ class ReplyItem extends StatelessWidget {
),
],
),
Row(
children: <Widget>[
Text(
Utils.dateFormat(replyItem!.ctime),
style: TextStyle(
fontSize: textTheme.labelSmall!.fontSize,
color: colorScheme.outline,
),
),
if (replyItem!.replyControl != null &&
replyItem!.replyControl!.location != '')
Text(
'${replyItem!.replyControl!.location!}',
RichText(
text: TextSpan(
children: [
TextSpan(
text: Utils.dateFormat(replyItem!.ctime),
style: TextStyle(
fontSize: textTheme.labelSmall!.fontSize,
color: colorScheme.outline),
fontSize: textTheme.labelSmall!.fontSize,
color: colorScheme.outline,
),
),
],
)
if (replyItem!.replyControl != null &&
replyItem!.replyControl!.location != '')
TextSpan(
text: '${replyItem!.replyControl!.location!}',
style: TextStyle(
fontSize: textTheme.labelSmall!.fontSize,
color: colorScheme.outline,
),
),
if (replyItem!.invisible!)
TextSpan(
text: ' • 隐藏的评论',
style: TextStyle(
color: colorScheme.outline,
fontSize: textTheme.labelSmall!.fontSize,
),
),
],
),
),
],
),
],
@ -698,14 +714,11 @@ InlineSpan buildContent(
'',
);
} else if (RegExp(r'^cv\d+$').hasMatch(matchStr)) {
Get.toNamed(
'/webview',
parameters: {
'url': 'https://www.bilibili.com/read/$matchStr',
'type': 'url',
'pageTitle': title
},
);
Get.toNamed('/read', parameters: {
'title': title,
'id': Utils.matchNum(matchStr).first.toString(),
'articleType': 'read',
});
} else {
Uri uri = Uri.parse(matchStr.replaceAll('/?', '?'));
SchemeEntity scheme = SchemeEntity(
@ -717,7 +730,7 @@ InlineSpan buildContent(
source: '',
dataString: matchStr,
);
PiliSchame.fullPathPush(scheme);
PiliSchame.httpsScheme(scheme);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
@ -1004,10 +1017,12 @@ InlineSpan buildContent(
class MorePanel extends StatelessWidget {
final dynamic item;
final bool mainFloor;
final bool isOwner;
const MorePanel({
super.key,
required this.item,
this.mainFloor = false,
this.isOwner = false,
});
Future<dynamic> menuActionHandler(String type) async {
@ -1043,9 +1058,43 @@ class MorePanel extends StatelessWidget {
// case 'report':
// SmartDialog.showToast('举报');
// break;
// case 'delete':
// SmartDialog.showToast('删除');
// break;
case 'delete':
// 删除评论提示
await showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('删除评论'),
content: const Text('删除评论后,评论下所有回复将被删除,确定删除吗?'),
actions: <Widget>[
TextButton(
onPressed: () => Get.back(),
child: Text('取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline)),
),
TextButton(
onPressed: () async {
Get.back();
var result = await ReplyHttp.replyDel(
type: item.type!,
oid: item.oid!,
rpid: item.rpid!,
);
if (result['status']) {
SmartDialog.showToast('评论删除成功,需手动刷新');
Get.back();
} else {
SmartDialog.showToast(result['msg']);
}
},
child: const Text('确定'),
),
],
);
},
);
break;
default:
}
}
@ -1054,6 +1103,7 @@ class MorePanel extends StatelessWidget {
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;
Color errorColor = colorScheme.error;
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
@ -1106,12 +1156,14 @@ class MorePanel extends StatelessWidget {
// leading: Icon(Icons.report_outlined, color: errorColor),
// title: Text('举报', style: TextStyle(color: errorColor)),
// ),
// ListTile(
// onTap: () async => await menuActionHandler('del'),
// minLeadingWidth: 0,
// leading: Icon(Icons.delete_outline, color: errorColor),
// title: Text('删除', style: TextStyle(color: errorColor)),
// ),
if (isOwner)
ListTile(
onTap: () async => await menuActionHandler('delete'),
minLeadingWidth: 0,
leading: Icon(Icons.delete_outline, color: errorColor),
title: Text('删除评论',
style: textTheme.titleSmall!.copyWith(color: errorColor)),
),
],
),
);

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:floating/floating.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_svg/svg.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -68,10 +69,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late final AppLifecycleListener _lifecycleListener;
late double statusHeight;
// 稍后再看控制器
// late AnimationController _laterCtr;
// late Animation<Offset> _laterOffsetAni;
@override
void initState() {
super.initState();
@ -85,14 +82,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController.videoDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
});
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
});
vdCtr.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0);
});
if (vdCtr.videoType == SearchType.media_bangumi) {
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
});
vdCtr.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0);
});
}
statusBarHeight = localCache.get('statusBarHeight');
autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
@ -108,7 +107,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
WidgetsBinding.instance.addObserver(this);
lifecycleListener();
// watchLaterControllerInit();
}
// 获取视频资源,初始化播放器
@ -242,8 +240,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStream.close();
WidgetsBinding.instance.removeObserver(this);
_lifecycleListener.dispose();
// _laterCtr.dispose();
// _laterOffsetAni.removeListener(() {});
super.dispose();
}
@ -297,6 +293,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController?.play();
}
plPlayerController?.addStatusLister(playerListener);
appbarStream.add(0);
super.didPopNext();
}
@ -490,21 +487,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
);
}
/// 稍后再看控制器初始化
// void watchLaterControllerInit() {
// _laterCtr = AnimationController(
// duration: const Duration(milliseconds: 300),
// vsync: this,
// );
// _laterOffsetAni = Tween<Offset>(
// begin: const Offset(0.0, 1.0),
// end: Offset.zero,
// ).animate(CurvedAnimation(
// parent: _laterCtr,
// curve: Curves.easeInOut,
// ));
// }
@override
Widget build(BuildContext context) {
final sizeContext = MediaQuery.sizeOf(context);
@ -529,6 +511,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
exitFullScreen();
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness:
Get.isDarkMode ? Brightness.light : Brightness.dark,
),
);
Widget buildLoadingWidget() {
return Center(child: Lottie.asset('assets/loading.json', width: 200));
}
@ -615,10 +605,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
key: vdCtr.scaffoldKey,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(0),
child: AppBar(
backgroundColor: Colors.black,
elevation: 0,
scrolledUnderElevation: 0,
child: StreamBuilder(
stream: appbarStream.stream.distinct(),
initialData: 0,
builder: ((context, snapshot) {
return AppBar(
backgroundColor: Colors.black,
elevation: 0,
scrolledUnderElevation: 0,
);
}),
),
),
body: ExtendedNestedScrollView(

View File

@ -109,7 +109,7 @@ class _MediaListPanelState extends State<MediaListPanel> {
var item = mediaList[index];
return InkWell(
onTap: () async {
String bvid = item.bvId!;
String bvid = item.bvid!;
int? aid = item.id;
String cover = item.cover ?? '';
final int cid =
@ -173,7 +173,7 @@ class _MediaListPanelState extends State<MediaListPanel> {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
color: item.bvId == widget.bvid
color: item.bvid == widget.bvid
? Theme.of(context)
.colorScheme
.primary

View File

@ -228,7 +228,7 @@ class SessionItem extends StatelessWidget {
parameters: {
'talkerId': sessionItem.talkerId.toString(),
'name': sessionItem.accountInfo.name,
'face': sessionItem.accountInfo.face,
'face': sessionItem.accountInfo.face ?? '',
'mid': (sessionItem.accountInfo?.mid ?? 0).toString(),
'heroTag': heroTag,
},
@ -244,7 +244,7 @@ class SessionItem extends StatelessWidget {
width: 45,
height: 45,
type: 'avatar',
src: sessionItem.accountInfo.face,
src: sessionItem.accountInfo.face ?? '',
),
),
),