mod: 侧边栏、动态重构,排行改为首页分区,平板、折叠屏、竖屏视频新适配,播放页可隐藏黑边、截图、点踩,弹幕粗细调整,默认关闭后台播放,弹窗接受返回

This commit is contained in:
orz12
2024-05-20 14:46:31 +08:00
parent fd51cddeca
commit 074bf03946
97 changed files with 4105 additions and 2672 deletions

View File

@@ -1,5 +1,6 @@
// ignore_for_file: avoid_print
import 'package:PiliPalaX/pages/dynamics/tab/index.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -17,39 +18,17 @@ import 'package:PiliPalaX/utils/id_utils.dart';
import 'package:PiliPalaX/utils/storage.dart';
import 'package:PiliPalaX/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
class DynamicsController extends GetxController
with GetTickerProviderStateMixin {
String? offset = '';
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
Rx<DynamicsType> dynamicsType = DynamicsType.values[0].obs;
RxString dynamicsTypeLabel = '全部'.obs;
final ScrollController scrollController = ScrollController();
Rx<FollowUpModel> upData = FollowUpModel().obs;
// 默认获取全部动态
RxInt mid = (-1).obs;
Rx<UpItem> upInfo = UpItem().obs;
List filterTypeList = [
{
'label': DynamicsType.all.labels,
'value': DynamicsType.all,
'enabled': true
},
{
'label': DynamicsType.video.labels,
'value': DynamicsType.video,
'enabled': true
},
{
'label': DynamicsType.pgc.labels,
'value': DynamicsType.pgc,
'enabled': true
},
{
'label': DynamicsType.article.labels,
'value': DynamicsType.article,
'enabled': true
},
];
late TabController tabController;
RxList<int> tempBannedList = <int>[].obs;
late List<Widget> tabsPageList;
bool flag = false;
RxInt initialValue = 0.obs;
Box userInfoCache = GStrorage.userInfo;
@@ -63,53 +42,23 @@ class DynamicsController extends GetxController {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
super.onInit();
initialValue.value =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
dynamicsType = DynamicsType.values[initialValue.value].obs;
tabController = TabController(
length: tabsConfig.length,
vsync: this,
initialIndex:
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0));
tabsPageList = tabsConfig.map((e) {
return e['page'] as Widget;
}).toList();
}
Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
}
if (type == 'init') {
dynamicsList.clear();
}
// 下拉刷新数据渲染时会触发onLoad
if (type == 'onLoad' && page == 1) {
return;
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: dynamicsType.value.values,
offset: offset,
mid: mid.value,
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'init') {
dynamicsList.value = res['data'].items;
} else {
dynamicsList.addAll(res['data'].items);
}
offset = res['data'].offset;
page++;
}
return res;
void refreshNotifier() {
queryFollowUp();
}
onSelectType(value) async {
dynamicsType.value = filterTypeList[value]['value'];
dynamicsList.value = [DynamicItemModel()];
page = 1;
initialValue.value = value;
await queryFollowDynamic();
scrollController.jumpTo(0);
}
pushDetail(item, floor, {action = 'all'}) async {
@@ -276,21 +225,27 @@ class DynamicsController extends GetxController {
}
onSelectUp(mid) async {
dynamicsType.value = DynamicsType.values[0];
dynamicsList.value = [DynamicItemModel()];
page = 1;
queryFollowDynamic();
if (mid == this.mid.value) {
this.mid.refresh();
return;
}
this.mid.value = mid;
tabController.index = (mid == -1 ? 0 : 4);
}
onRefresh() async {
page = 1;
print('onRefresh');
await queryFollowUp();
await queryFollowDynamic();
print(tabsConfig[tabController.index]['ctr']);
await Future.wait(<Future>[
queryFollowUp(),
tabsConfig[tabController.index]['ctr'].onRefresh()
]);
}
// 返回顶部并刷新
void animateToTop() async {
tabsConfig[tabController.index]['ctr'].animateToTop();
if (!scrollController.hasClients) return;
if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) {
scrollController.jumpTo(0);
@@ -299,14 +254,4 @@ class DynamicsController extends GetxController {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
// 重置搜索
void resetSearch() {
mid.value = -1;
dynamicsType.value = DynamicsType.values[0];
initialValue.value = 0;
SmartDialog.showToast('还原默认加载');
dynamicsList.value = [DynamicItemModel()];
queryFollowDynamic();
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
// import 'package:hive/hive.dart';
import '../../../http/dynamics.dart';
import '../../../models/dynamics/result.dart';
// import '../../../utils/storage.dart';
class DynamicsTabController extends GetxController {
String offset = '';
ScrollController scrollController = ScrollController();
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
RxBool isLoadingMore = false.obs;
String dynamicsType = 'all';
// Box userInfoCache = GStrorage.userInfo;
// bool userLogin = false;
int mid = -1;
Future queryFollowDynamic(String type, String dynamicsType, int? mid) async {
this.dynamicsType = dynamicsType;
if (mid != null) this.mid = mid;
if (type != 'onLoad') {
dynamicsList.clear();
offset = '';
}
// // 下拉刷新数据渲染时会触发onLoad
// if (type == 'onLoad' && page == 1) {
// return;
// }
isLoadingMore.value = true;
var res = await DynamicsHttp.followDynamic(
type: dynamicsType == "up" ? "all" : dynamicsType,
offset: offset,
mid: dynamicsType == "up" ? mid : -1,
);
isLoadingMore.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'onLoad') {
dynamicsList.addAll(res['data'].items);
} else {
dynamicsList.value = res['data'].items;
}
// print('dynamicsList: $dynamicsList');
dynamicsList.refresh();
offset = res['data'].offset;
// print("page: $page[dynamicsType]!");
}
return res;
}
// 下拉刷新
Future onRefresh() async {
await queryFollowDynamic('onRefresh', dynamicsType, mid);
}
// 上拉加载
Future onLoad() async {
await queryFollowDynamic('onLoad', dynamicsType, mid);
}
// 返回顶部并刷新
void animateToTop() async {
if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) {
scrollController.jumpTo(0);
} else {
await scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
}

View File

@@ -0,0 +1,4 @@
library dynamics.tab;
export './controller.dart';
export './view.dart';

View File

@@ -0,0 +1,234 @@
import 'dart:async';
import 'dart:math';
import 'package:PiliPalaX/utils/storage.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:PiliPalaX/common/constants.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/pages/home/index.dart';
import 'package:PiliPalaX/pages/main/index.dart';
import 'package:PiliPalaX/common/widgets/no_data.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:waterfall_flow/waterfall_flow.dart';
import '../../../common/skeleton/dynamic_card.dart';
import '../../../models/dynamics/result.dart';
import '../../../utils/grid.dart';
import '../index.dart';
import '../widgets/dynamic_panel.dart';
import 'controller.dart';
class DynamicsTabPage extends StatefulWidget {
const DynamicsTabPage({Key? key, required this.dynamicsType})
: super(key: key);
final String dynamicsType;
@override
State<DynamicsTabPage> createState() => _DynamicsTabPageState();
}
class _DynamicsTabPageState extends State<DynamicsTabPage>
with AutomaticKeepAliveClientMixin {
late DynamicsTabController _dynamicsTabController;
late Future _futureBuilderFuture;
late ScrollController scrollController;
late bool dynamicsWaterfallFlow;
late final DynamicsController dynamicsController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_dynamicsTabController =
Get.put(DynamicsTabController(), tag: widget.dynamicsType);
dynamicsController = Get.find<DynamicsController>();
_futureBuilderFuture = _dynamicsTabController.queryFollowDynamic(
'init', widget.dynamicsType, dynamicsController.mid.value);
scrollController = _dynamicsTabController.scrollController
..addListener(() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_dynamicsTabController.isLoadingMore.value) {
EasyThrottle.throttle('_dynamicsTabController_onLoad',
const Duration(milliseconds: 500), () {
_dynamicsTabController.isLoadingMore.value = true;
_dynamicsTabController.onLoad();
_dynamicsTabController.isLoadingMore.value = false;
});
}
}
});
dynamicsController.mid.listen((mid) {
print('midListen: $mid');
scrollController.jumpTo(0);
_futureBuilderFuture = _dynamicsTabController.queryFollowDynamic(
'init', widget.dynamicsType, mid);
});
dynamicsWaterfallFlow = GStrorage.setting
.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);
}
@override
void dispose() {
scrollController.removeListener(() {});
dynamicsController.mid.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
// print(widget.dynamicsType + widget.mid.value.toString());
return RefreshIndicator(
// key:
// ValueKey<String>(widget.dynamicsType + widget.mid.value.toString()),
onRefresh: () async {
dynamicsWaterfallFlow = GStrorage.setting
.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);
await Future.wait(<Future>[
_dynamicsTabController.onRefresh(),
dynamicsController.queryFollowUp()
]);
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _dynamicsTabController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
// print(snapshot);
// print(widget.dynamicsType + "${widget.mid?.value}");
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const NoData();
}
Map data = snapshot.data;
// print('data: $data');
if (data['status']) {
List<DynamicItemModel> list =
_dynamicsTabController.dynamicsList;
// print('list: $list');
return Obx(
() {
if (list.isEmpty) {
if (_dynamicsTabController.isLoadingMore.value) {
return skeleton();
}
return const NoData();
}
if (!dynamicsWaterfallFlow) {
return SliverCrossAxisGroup(
slivers: [
const SliverFillRemaining(),
SliverConstrainedCrossAxis(
maxExtent: Grid.maxRowWidth * 2,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if ((dynamicsController
.tabController.index ==
4 &&
dynamicsController.mid.value !=
-1) ||
!dynamicsController.tempBannedList
.contains(list[index]
.modules
?.moduleAuthor
?.mid)) {
return DynamicPanel(
item: list[index]);
}
return const SizedBox();
},
childCount: list.length,
),
)),
const SliverFillRemaining(),
],
);
}
return SliverWaterfallFlow.extent(
maxCrossAxisExtent: Grid.maxRowWidth * 2,
//cacheExtent: 0.0,
crossAxisSpacing: StyleString.cardSpace / 2,
mainAxisSpacing: StyleString.cardSpace / 2,
lastChildLayoutTypeBuilder: (index) =>
index == list.length
? LastChildLayoutType.foot
: LastChildLayoutType.none,
children: [
if (dynamicsController.tabController.index == 4 &&
dynamicsController.mid.value != -1) ...[
for (var i in list) DynamicPanel(item: i),
] else ...[
for (var i in list)
if (!dynamicsController.tempBannedList
.contains(i.modules?.moduleAuthor?.mid))
DynamicPanel(item: i),
]
],
);
},
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () {
// setState(() {
_futureBuilderFuture =
_dynamicsTabController.queryFollowDynamic(
'init',
widget.dynamicsType,
dynamicsController.mid.value);
// });
dynamicsController.onRefresh();
},
);
}
} else {
// 骨架屏
return skeleton();
}
},
)
],
));
}
Widget skeleton() {
if (!dynamicsWaterfallFlow) {
return SliverCrossAxisGroup(slivers: [
const SliverFillRemaining(),
SliverConstrainedCrossAxis(
maxExtent: Grid.maxRowWidth * 2,
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 10)),
),
const SliverFillRemaining()
]);
}
return SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
crossAxisSpacing: StyleString.cardSpace / 2,
mainAxisSpacing: StyleString.cardSpace / 2,
maxCrossAxisExtent: Grid.maxRowWidth * 2,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent: 50),
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 10),
);
}
}

View File

@@ -1,24 +1,15 @@
import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:PiliPalaX/models/common/dynamics_type.dart';
import 'package:PiliPalaX/models/common/up_panel_position.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:waterfall_flow/waterfall_flow.dart';
import 'package:PiliPalaX/common/skeleton/dynamic_card.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/common/widgets/no_data.dart';
import 'package:PiliPalaX/models/dynamics/result.dart';
import 'package:PiliPalaX/pages/main/index.dart';
import 'package:PiliPalaX/utils/feed_back.dart';
import 'package:PiliPalaX/utils/storage.dart';
import '../../common/constants.dart';
import '../../utils/grid.dart';
import 'controller.dart';
import 'widgets/dynamic_panel.dart';
import 'widgets/up_panel.dart';
class DynamicsPage extends StatefulWidget {
@@ -29,12 +20,12 @@ class DynamicsPage extends StatefulWidget {
}
class _DynamicsPageState extends State<DynamicsPage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
final DynamicsController _dynamicsController = Get.put(DynamicsController());
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
Box userInfoCache = GStrorage.userInfo;
late ScrollController scrollController;
late UpPanelPosition upPanelPosition;
@override
bool get wantKeepAlive => true;
@@ -42,269 +33,125 @@ class _DynamicsPageState extends State<DynamicsPage>
@override
void initState() {
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
scrollController = _dynamicsController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
_dynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
},
);
// _dynamicsController.tabController =
// TabController(vsync: this, length: DynamicsType.values.length);
// ..addListener(() {
// if (!_dynamicsController.tabController.indexIsChanging) {
// // if (!mounted) return;
// // print('indexChanging: ${_dynamicsController.tabController.index}');
// _dynamicsController
// .onSelectType(_dynamicsController.tabController.index);
// }
// });
_dynamicsController.userLogin.listen((status) {
if (mounted) {
setState(() {
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
});
}
});
upPanelPosition = UpPanelPosition.values[setting.get(
SettingBoxKey.upPanelPosition,
defaultValue: UpPanelPosition.leftFixed.code)];
print('upPanelPosition: $upPanelPosition');
scrollController = _dynamicsController.scrollController;
}
@override
void dispose() {
scrollController.removeListener(() {});
_dynamicsController.tabController.removeListener(() {});
_dynamicsController.tabController.dispose();
super.dispose();
}
Widget upPanelPart() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Container(
//抽屉模式增加底色
color: upPanelPosition.code > 1? Theme.of(context).colorScheme.surface: Colors.transparent,
width: 56,
child: FutureBuilder(
future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(
_dynamicsController.upData.value,
scrollController));
} else {
return const SizedBox();
}
} else {
return const SizedBox(
width: 56,
child: UpPanelSkeleton(),
);
}
},
),
));
}
@override
Widget build(BuildContext context) {
print('upPanelPosition1: $upPanelPosition');
super.build(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
titleSpacing: 0,
title: SizedBox(
height: 34,
child: Stack(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
if (_dynamicsController.mid.value != -1 &&
_dynamicsController.upInfo.value.uname != null) {
return SizedBox(
height: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'${_dynamicsController.upInfo.value.uname!}的动态',
key: ValueKey<String>(
_dynamicsController.upInfo.value.uname!),
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelLarge!
.fontSize,
)),
),
);
} else {
return const SizedBox();
}
}),
Obx(
() => _dynamicsController.userLogin.value
? Visibility(
visible: _dynamicsController.mid.value == -1,
child: Theme(
data: ThemeData(
splashColor:
Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor:
Colors.transparent, // 点击时的背景高亮颜色设置为透明
),
child: CustomSlidingSegmentedControl<int>(
initialValue:
_dynamicsController.initialValue.value,
children: {
0: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
),
1: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
2: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('专栏',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color:
Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
),
)
: Text('动态',
style: Theme.of(context).textTheme.titleMedium),
)
],
),
],
),
),
),
body: RefreshIndicator(
onRefresh: () => _dynamicsController.onRefresh(),
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _dynamicsController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value));
} else {
return const SliverToBoxAdapter(
child: SizedBox(height: 80),
);
}
} else {
return const SliverToBoxAdapter(
child: SizedBox(
height: 90,
child: UpPanelSkeleton(),
));
backgroundColor: Colors.transparent,
appBar: AppBar(
toolbarHeight: 50,
elevation: 0,
backgroundColor: Colors.transparent,
title: SizedBox(
height: 50,
child: TabBar(
controller: _dynamicsController.tabController,
isScrollable: true,
dividerColor: Colors.transparent,
dividerHeight: 0,
tabAlignment: TabAlignment.center,
indicatorColor: Theme.of(context).colorScheme.primary,
labelColor: Theme.of(context).colorScheme.primary,
unselectedLabelColor: Theme.of(context).colorScheme.onSurface,
labelStyle: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
),
tabs: DynamicsType.values
.map((e) => Tab(text: e.labels))
.toList(),
onTap: (index) {
print('index: $index');
feedBack();
tabsConfig[_dynamicsController.tabController.index]['ctr'].animateToTop();
// _dynamicsController.tabController
// _dynamicsController.tabController.index = index;
// _dynamicsController.onSelectType(index);
// _
}
},
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
List<DynamicItemModel> list =
_dynamicsController.dynamicsList;
return Obx(
() {
if (list.isEmpty) {
if (_dynamicsController.isLoadingDynamic.value) {
return skeleton();
} else {
return const NoData();
}
} else {
return SliverWaterfallFlow.extent(
maxCrossAxisExtent: Grid.maxRowWidth * 2,
//cacheExtent: 0.0,
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.cardSpace,
/// follow max child trailing layout offset and layout with full cross axis extend
/// last child as loadmore item/no more item in [GridView] and [WaterfallFlow]
/// with full cross axis extend
// LastChildLayoutType.fullCrossAxisExtend,
/// as foot at trailing and layout with full cross axis extend
/// show no more item at trailing when children are not full of viewport
/// if children is full of viewport, it's the same as fullCrossAxisExtend
// LastChildLayoutType.foot,
lastChildLayoutTypeBuilder: (index) =>
index == list.length
? LastChildLayoutType.foot
: LastChildLayoutType.none,
children: [
for (var i in list) DynamicPanel(item: i),
]);
}
},
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
},
);
}
} else {
// 骨架屏
return skeleton();
}
},
),
const SliverToBoxAdapter(child: SizedBox(height: 40))
],
)),
),
),
);
}
Widget skeleton() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 5),
);
drawer: upPanelPosition == UpPanelPosition.leftDrawer ?
upPanelPart(): null,
drawerEnableOpenDragGesture: true,
endDrawer: upPanelPosition == UpPanelPosition.rightDrawer ?
upPanelPart(): null,
endDrawerEnableOpenDragGesture: true,
body: Row(children: [
if (upPanelPosition == UpPanelPosition.leftFixed)
upPanelPart(),
Expanded(
child: TabBarView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _dynamicsController.tabController,
children: _dynamicsController.tabsPageList,
)),
if (upPanelPosition == UpPanelPosition.rightFixed)
upPanelPart(),
]));
}
}

View File

@@ -5,10 +5,15 @@ import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
import 'package:PiliPalaX/http/user.dart';
import 'package:PiliPalaX/utils/feed_back.dart';
import 'package:PiliPalaX/utils/utils.dart';
import 'package:share_plus/share_plus.dart';
import '../../../http/constants.dart';
import '../controller.dart';
class AuthorPanel extends StatelessWidget {
final dynamic item;
const AuthorPanel({super.key, required this.item});
final Function? addBannedList;
const AuthorPanel({super.key, required this.item, this.addBannedList});
@override
Widget build(BuildContext context) {
@@ -77,28 +82,27 @@ class AuthorPanel extends StatelessWidget {
],
),
const Spacer(),
if (item.type == 'DYNAMIC_TYPE_AV')
SizedBox(
width: 32,
height: 32,
child: IconButton(
tooltip: '更多',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: item);
},
);
},
icon: const Icon(Icons.more_vert_outlined, size: 18),
SizedBox(
width: 32,
height: 32,
child: IconButton(
tooltip: '更多',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: item);
},
);
},
icon: const Icon(Icons.more_vert_outlined, size: 18),
),
),
],
);
}
@@ -132,24 +136,56 @@ class MorePanel extends StatelessWidget {
),
),
),
if (item.type == 'DYNAMIC_TYPE_AV')
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
'分享动态',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.share_outlined, size: 19),
onTap: () async {
var result = await Share.share(
'${HttpString.baseUrl}/dynamic/${item.idStr} UP主: ${item.modules.moduleAuthor.name}')
.whenComplete(() {});
return result;
},
minLeadingWidth: 0,
),
ListTile(
title: Text(
'临时屏蔽:${item.modules.moduleAuthor.name}',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.visibility_off_outlined, size: 19),
onTap: () {
Get.back();
DynamicsController dynamicsController =
Get.find<DynamicsController>();
dynamicsController.tempBannedList
.add(item.modules.moduleAuthor.mid);
SmartDialog.showToast(
'已临时屏蔽${item.modules.moduleAuthor.name}(${item.modules.moduleAuthor.mid}),重启恢复');
},
minLeadingWidth: 0,
),
const Divider(thickness: 0.1, height: 1),
ListTile(

View File

@@ -1,3 +1,4 @@
import 'package:PiliPalaX/models/dynamics/result.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:PiliPalaX/pages/dynamics/index.dart';
@@ -9,7 +10,7 @@ import 'forward_panel.dart';
class DynamicPanel extends StatelessWidget {
final dynamic item;
final String? source;
DynamicPanel({this.item, this.source, Key? key}) : super(key: key);
DynamicPanel({required this.item, this.source, Key? key}) : super(key: key);
final DynamicsController _dynamicsController = Get.put(DynamicsController());
@override
@@ -18,19 +19,20 @@ class DynamicPanel extends StatelessWidget {
padding: source == 'detail'
? const EdgeInsets.only(bottom: 12)
: EdgeInsets.zero,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 8,
color: Theme.of(context).dividerColor.withOpacity(0.05),
),
),
),
// decoration: BoxDecoration(
// border: Border(
// bottom: BorderSide(
// width: 8,
// color: Theme.of(context).dividerColor.withOpacity(0.05),
// ),
// ),
// ),
child: Material(
elevation: 0,
clipBehavior: Clip.hardEdge,
color: Theme.of(context).cardColor.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
borderRadius: BorderRadius.circular(5),
),
child: InkWell(
onTap: () => _dynamicsController.pushDetail(item, 1),
@@ -38,7 +40,7 @@ class DynamicPanel extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
padding: const EdgeInsets.fromLTRB(12, 12, 12, 6),
child: AuthorPanel(item: item),
),
if (item!.modules!.moduleDynamic!.desc != null ||

View File

@@ -11,26 +11,27 @@ import 'package:PiliPalaX/utils/utils.dart';
class UpPanel extends StatefulWidget {
final FollowUpModel? upData;
const UpPanel(this.upData, {Key? key}) : super(key: key);
final ScrollController scrollController;
const UpPanel(this.upData, this.scrollController, {Key? key}) : super(key: key);
@override
State<UpPanel> createState() => _UpPanelState();
}
class _UpPanelState extends State<UpPanel> {
final ScrollController scrollController = ScrollController();
int currentMid = -1;
late double contentWidth = 56;
List<UpItem> upList = [];
List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
Box userInfoCache = GStrorage.userInfo;
var userInfo;
bool _showLiveItems = false;
late DynamicsController dynamicsController;
@override
void initState() {
super.initState();
userInfo = userInfoCache.get('userInfoCache');
dynamicsController = Get.find<DynamicsController>();
}
@override
@@ -39,95 +40,81 @@ class _UpPanelState extends State<UpPanel> {
if (widget.upData!.liveUsers != null) {
liveList = widget.upData!.liveUsers!.items!;
}
return SliverPersistentHeader(
floating: true,
pinned: false,
delegate: _SliverHeaderDelegate(
height: 126,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
// return const SizedBox();
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: widget.scrollController,
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 45,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(const EdgeInsets.only()),
),
child: Column(
children: [
const Text('最新关注'),
GestureDetector(
onTap: () {
feedBack();
Get.toNamed('/follow?mid=${userInfo.mid}');
},
child: Container(
padding: const EdgeInsets.only(top: 5, bottom: 5),
child: Text(
'查看全部',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
const Spacer(),
const SizedBox(height: 12),
Text(
'Live(${liveList.length})',
style: const TextStyle(
fontSize: 13,
),
semanticsLabel:
'${_showLiveItems ? '展开' : '收起'}直播中的${liveList.length}个Up',
),
Icon(_showLiveItems ? Icons.expand_less : Icons.expand_more,
size: 12),
const Spacer(),
],
),
onPressed: () {
setState(() {
_showLiveItems = !_showLiveItems;
});
},
),
Container(
height: 90,
color: Theme.of(context).colorScheme.background,
child: Row(
children: [
Expanded(
child: ListView(
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
controller: scrollController,
children: [
const SizedBox(width: 10),
if (liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
),
],
upItemBuild(
UpItem(face: '', uname: '全部动态', mid: -1), 0),
upItemBuild(
UpItem(
face: userInfo.face,
uname: '',
mid: userInfo.mid,
),
1),
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i + 2)
],
const SizedBox(width: 10),
],
),
),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 10,
),
),
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
mainAxisExtent: 76,
crossAxisSpacing: 0,
mainAxisSpacing: 0,
),
delegate: SliverChildListDelegate(
[
if (_showLiveItems && liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
],
),
),
Container(
height: 6,
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
),
],
)),
);
upItemBuild(UpItem(face: '', uname: '全部动态', mid: -1), 0),
upItemBuild(
UpItem(
face: userInfo.face,
uname: '',
mid: userInfo.mid,
),
1),
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i + 2)
],
],
)),
const SliverToBoxAdapter(
child: SizedBox(
height: 200,
),
),
]);
}
Widget upItemBuild(data, i) {
@@ -137,28 +124,27 @@ class _UpPanelState extends State<UpPanel> {
feedBack();
if (data.type == 'up') {
currentMid = data.mid;
Get.find<DynamicsController>().mid.value = data.mid;
Get.find<DynamicsController>().upInfo.value = data;
Get.find<DynamicsController>().onSelectUp(data.mid);
int liveLen = liveList.length;
int upLen = upList.length;
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.sizeOf(context).width;
double moveDistance = 0.0;
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance =
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
}
// dynamicsController.mid.value = data.mid;
dynamicsController.upInfo.value = data;
dynamicsController.onSelectUp(data.mid);
// int liveLen = liveList.length;
// int upLen = upList.length;
// double itemWidth = contentWidth + itemPadding.horizontal;
// double screenWidth = MediaQuery.sizeOf(context).width;
// double moveDistance = 0.0;
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
// moveDistance =
// (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
// } else {
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
// }
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
// scrollController.animateTo(
// moveDistance,
// duration: const Duration(milliseconds: 500),
// curve: Curves.easeInOut,
// );
setState(() {});
} else if (data.type == 'live') {
LiveItemModel liveItem = LiveItemModel.fromJson({
@@ -182,63 +168,59 @@ class _UpPanelState extends State<UpPanel> {
Get.toNamed('/member?mid=${data.mid}',
arguments: {'face': data.face, 'heroTag': heroTag});
},
child: Padding(
padding: itemPadding,
child: AnimatedOpacity(
opacity: isCurrent ? 1 : 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
smallSize: 8,
label: data.type == 'live' ? const Text('Live') : null,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
alignment: data.type == 'live'
? AlignmentDirectional.topCenter
: AlignmentDirectional.topEnd,
padding: const EdgeInsets.only(left: 6, right: 6),
isLabelVisible: data.type == 'live' ||
(data.type == 'up' && (data.hasUpdate ?? false)),
backgroundColor: data.type == 'live'
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primary,
child: data.face != ''
? NetworkImgLayer(
width: 50,
height: 50,
src: data.face,
type: 'avatar',
)
: const CircleAvatar(
radius: 25,
backgroundImage: AssetImage(
'assets/images/noface.jpeg',
),
child: AnimatedOpacity(
opacity: isCurrent ? 1 : 0.6,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
smallSize: 8,
label: data.type == 'live' ? const Text('Live') : null,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
alignment: data.type == 'live'
? AlignmentDirectional.topCenter
: AlignmentDirectional.topEnd,
padding: const EdgeInsets.only(left: 6, right: 6),
isLabelVisible: data.type == 'live' ||
(data.type == 'up' && (data.hasUpdate ?? false)),
backgroundColor: data.type == 'live'
? Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.7)
: Theme.of(context).colorScheme.primary,
child: data.face != ''
? NetworkImgLayer(
width: 38,
height: 38,
src: data.face,
type: 'avatar',
)
: const CircleAvatar(
radius: 19,
backgroundImage: AssetImage(
'assets/images/logo/logo_android_2.png',
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: contentWidth,
child: Text(
data.uname,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid == data.mid
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize),
),
),
),
],
),
),
),
const SizedBox(height: 3),
Text(
data.uname,
overflow: TextOverflow.clip,
maxLines: 2,
softWrap: true,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid == data.mid
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
height: 1.1,
fontSize: 12.5),
),
],
),
),
);
@@ -273,35 +255,25 @@ class UpPanelSkeleton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(50),
),
),
Container(
margin: const EdgeInsets.only(top: 6),
width: 45,
height: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(50),
),
);
}),
),
Container(
margin: const EdgeInsets.only(top: 6),
width: 45,
height: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
);
}
}

View File

@@ -105,8 +105,8 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
right: 0,
bottom: 0,
child: Container(
height: 80,
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
height: 70,
padding: const EdgeInsets.fromLTRB(10, 0, 8, 8),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
gradient: const LinearGradient(
@@ -139,17 +139,17 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
'时长${Utils.durationReadFormat(content.durationText)}',
),
if (content.durationText != null)
const SizedBox(width: 10),
const SizedBox(width: 6),
Text(content.stat.play + '次围观'),
const SizedBox(width: 10),
const SizedBox(width: 6),
Text(content.stat.danmu + '条弹幕')
],
),
),
Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
width: 50,
height: 50,
),
],
),