feat: 动态筛选&UP主筛选
This commit is contained in:
@ -134,6 +134,7 @@ class Api {
|
|||||||
|
|
||||||
// 正在直播的up & 关注的up
|
// 正在直播的up & 关注的up
|
||||||
// https://api.bilibili.com/x/polymer/web-dynamic/v1/portal
|
// https://api.bilibili.com/x/polymer/web-dynamic/v1/portal
|
||||||
|
static const String followUp = '/x/polymer/web-dynamic/v1/portal';
|
||||||
|
|
||||||
// 关注的up动态
|
// 关注的up动态
|
||||||
// https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all
|
// https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import 'package:pilipala/http/index.dart';
|
import 'package:pilipala/http/index.dart';
|
||||||
import 'package:pilipala/models/dynamics/result.dart';
|
import 'package:pilipala/models/dynamics/result.dart';
|
||||||
|
import 'package:pilipala/models/dynamics/up.dart';
|
||||||
|
|
||||||
class DynamicsHttp {
|
class DynamicsHttp {
|
||||||
static Future followDynamic({
|
static Future followDynamic({
|
||||||
String? type,
|
String? type,
|
||||||
int? page,
|
int? page,
|
||||||
String? offset,
|
String? offset,
|
||||||
|
int? mid,
|
||||||
}) async {
|
}) async {
|
||||||
var res = await Request().get(Api.followDynamic, data: {
|
Map<String, dynamic> data = {
|
||||||
'type': type ?? 'all',
|
'type': type ?? 'all',
|
||||||
'page': page ?? 1,
|
'page': page ?? 1,
|
||||||
'timezone_offset': '-480',
|
'timezone_offset': '-480',
|
||||||
'offset': page == 1 ? '' : offset,
|
'offset': page == 1 ? '' : offset,
|
||||||
'features': 'itemOpusStyle'
|
'features': 'itemOpusStyle'
|
||||||
});
|
};
|
||||||
|
if (mid != -1) {
|
||||||
|
data['host_mid'] = mid;
|
||||||
|
data.remove('timezone_offset');
|
||||||
|
}
|
||||||
|
var res = await Request().get(Api.followDynamic, data: data);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return {
|
return {
|
||||||
'status': true,
|
'status': true,
|
||||||
@ -27,4 +34,20 @@ class DynamicsHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future followUp() async {
|
||||||
|
var res = await Request().get(Api.followUp);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': FollowUpModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@ enum DynamicsType {
|
|||||||
|
|
||||||
extension BusinessTypeExtension on DynamicsType {
|
extension BusinessTypeExtension on DynamicsType {
|
||||||
String get values => ['all', 'video', 'pgc', 'article'][index];
|
String get values => ['all', 'video', 'pgc', 'article'][index];
|
||||||
String get labels => ['全部', '视频投稿', '追番追剧', '专栏'][index];
|
String get labels => ['全部', '视频', '追番', '专栏'][index];
|
||||||
}
|
}
|
||||||
|
@ -526,7 +526,7 @@ class OpusPicsModel {
|
|||||||
OpusPicsModel.fromJson(Map<String, dynamic> json) {
|
OpusPicsModel.fromJson(Map<String, dynamic> json) {
|
||||||
width = json['width'];
|
width = json['width'];
|
||||||
height = json['height'];
|
height = json['height'];
|
||||||
size = json['size'].toInt();
|
size = json['size'] != null ? json['size'].toInt() : 0;
|
||||||
src = json['src'];
|
src = json['src'];
|
||||||
url = json['url'];
|
url = json['url'];
|
||||||
}
|
}
|
||||||
|
93
lib/models/dynamics/up.dart
Normal file
93
lib/models/dynamics/up.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
class FollowUpModel {
|
||||||
|
FollowUpModel({
|
||||||
|
this.liveUsers,
|
||||||
|
this.upList,
|
||||||
|
});
|
||||||
|
|
||||||
|
LiveUsers? liveUsers;
|
||||||
|
List<UpItem>? upList;
|
||||||
|
|
||||||
|
FollowUpModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
liveUsers = LiveUsers.fromJson(json['live_users']);
|
||||||
|
upList = json['up_list'] != null
|
||||||
|
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveUsers {
|
||||||
|
LiveUsers({
|
||||||
|
this.count,
|
||||||
|
this.group,
|
||||||
|
this.items,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? count;
|
||||||
|
String? group;
|
||||||
|
List<LiveUserItem>? items;
|
||||||
|
|
||||||
|
LiveUsers.fromJson(Map<String, dynamic> json) {
|
||||||
|
count = json['count'];
|
||||||
|
group = json['group'];
|
||||||
|
items = json['items']
|
||||||
|
.map<LiveUserItem>((e) => LiveUserItem.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveUserItem {
|
||||||
|
LiveUserItem({
|
||||||
|
this.face,
|
||||||
|
this.isReserveRecall,
|
||||||
|
this.jumpUrl,
|
||||||
|
this.mid,
|
||||||
|
this.roomId,
|
||||||
|
this.title,
|
||||||
|
this.uname,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? face;
|
||||||
|
bool? isReserveRecall;
|
||||||
|
String? jumpUrl;
|
||||||
|
int? mid;
|
||||||
|
int? roomId;
|
||||||
|
String? title;
|
||||||
|
String? uname;
|
||||||
|
bool hasUpdate = false;
|
||||||
|
String type = 'live';
|
||||||
|
|
||||||
|
LiveUserItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
face = json['face'];
|
||||||
|
isReserveRecall = json['is_reserve_recall'];
|
||||||
|
jumpUrl = json['jump_url'];
|
||||||
|
mid = json['mid'];
|
||||||
|
roomId = json['room_id'];
|
||||||
|
title = json['title'];
|
||||||
|
uname = json['uname'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpItem {
|
||||||
|
UpItem({
|
||||||
|
this.face,
|
||||||
|
this.hasUpdate,
|
||||||
|
this.isReserveRecall,
|
||||||
|
this.mid,
|
||||||
|
this.uname,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? face;
|
||||||
|
bool? hasUpdate;
|
||||||
|
bool? isReserveRecall;
|
||||||
|
int? mid;
|
||||||
|
String? uname;
|
||||||
|
String type = 'up';
|
||||||
|
|
||||||
|
UpItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
face = json['face'];
|
||||||
|
hasUpdate = json['has_update'];
|
||||||
|
isReserveRecall = json['is_reserve_recall'];
|
||||||
|
mid = json['mid'];
|
||||||
|
uname = json['uname'];
|
||||||
|
}
|
||||||
|
}
|
@ -3,22 +3,53 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/http/dynamics.dart';
|
import 'package:pilipala/http/dynamics.dart';
|
||||||
import 'package:pilipala/http/search.dart';
|
import 'package:pilipala/http/search.dart';
|
||||||
|
import 'package:pilipala/models/common/dynamics_type.dart';
|
||||||
import 'package:pilipala/models/dynamics/result.dart';
|
import 'package:pilipala/models/dynamics/result.dart';
|
||||||
|
import 'package:pilipala/models/dynamics/up.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
class DynamicsController extends GetxController {
|
class DynamicsController extends GetxController {
|
||||||
int page = 1;
|
int page = 1;
|
||||||
String? offset = '';
|
String? offset = '';
|
||||||
RxList<DynamicItemModel>? dynamicsList = [DynamicItemModel()].obs;
|
RxList<DynamicItemModel>? dynamicsList = [DynamicItemModel()].obs;
|
||||||
RxString dynamicsType = 'all'.obs;
|
Rx<DynamicsType> dynamicsType = DynamicsType.values[0].obs;
|
||||||
RxString dynamicsTypeLabel = '全部'.obs;
|
RxString dynamicsTypeLabel = '全部'.obs;
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
Rx<FollowUpModel> upData = FollowUpModel().obs;
|
||||||
|
// 默认获取全部动态
|
||||||
|
int mid = -1;
|
||||||
|
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
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
Future queryFollowDynamic({type = 'init'}) async {
|
Future queryFollowDynamic({type = 'init'}) async {
|
||||||
|
// if (type == 'init') {
|
||||||
|
// dynamicsList!.value = [];
|
||||||
|
// }
|
||||||
var res = await DynamicsHttp.followDynamic(
|
var res = await DynamicsHttp.followDynamic(
|
||||||
page: type == 'init' ? 1 : page,
|
page: type == 'init' ? 1 : page,
|
||||||
type: dynamicsType.value,
|
type: dynamicsType.value.values,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
|
mid: mid,
|
||||||
);
|
);
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
if (type == 'init') {
|
if (type == 'init') {
|
||||||
@ -32,12 +63,10 @@ class DynamicsController extends GetxController {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectType(value, label) async {
|
onSelectType(value) async {
|
||||||
dynamicsType.value = value;
|
dynamicsType.value = value;
|
||||||
dynamicsTypeLabel.value = label;
|
|
||||||
await queryFollowDynamic();
|
await queryFollowDynamic();
|
||||||
scrollController.animateTo(0,
|
scrollController.jumpTo(0);
|
||||||
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pushDetail(item, floor, {action = 'all'}) async {
|
pushDetail(item, floor, {action = 'all'}) async {
|
||||||
@ -86,4 +115,18 @@ class DynamicsController extends GetxController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future queryFollowUp() async {
|
||||||
|
var res = await DynamicsHttp.followUp();
|
||||||
|
if (res['status']) {
|
||||||
|
upData.value = res['data'];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectUp(mid) async {
|
||||||
|
dynamicsType.value = DynamicsType.values[0];
|
||||||
|
|
||||||
|
queryFollowDynamic();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,9 @@ class DynamicDetailController extends GetxController {
|
|||||||
} else {
|
} else {
|
||||||
replyList.addAll(replies);
|
replyList.addAll(replies);
|
||||||
}
|
}
|
||||||
|
if (replyList.length == acount.value) {
|
||||||
|
noMore.value = '没有更多了';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isLoadingMore = false;
|
isLoadingMore = false;
|
||||||
return res;
|
return res;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/skeleton/dynamic_card.dart';
|
import 'package:pilipala/common/skeleton/dynamic_card.dart';
|
||||||
import 'package:pilipala/common/widgets/http_error.dart';
|
import 'package:pilipala/common/widgets/http_error.dart';
|
||||||
@ -7,6 +8,7 @@ import 'package:pilipala/models/dynamics/result.dart';
|
|||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
import 'widgets/dynamic_panel.dart';
|
import 'widgets/dynamic_panel.dart';
|
||||||
|
import 'widgets/up_panel.dart';
|
||||||
|
|
||||||
class DynamicsPage extends StatefulWidget {
|
class DynamicsPage extends StatefulWidget {
|
||||||
const DynamicsPage({super.key});
|
const DynamicsPage({super.key});
|
||||||
@ -19,7 +21,6 @@ class _DynamicsPageState extends State<DynamicsPage>
|
|||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
final DynamicsController _dynamicsController = Get.put(DynamicsController());
|
final DynamicsController _dynamicsController = Get.put(DynamicsController());
|
||||||
Future? _futureBuilderFuture;
|
Future? _futureBuilderFuture;
|
||||||
// final ScrollController scrollController = ScrollController();
|
|
||||||
bool _isLoadingMore = false;
|
bool _isLoadingMore = false;
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
@ -48,81 +49,140 @@ class _DynamicsPageState extends State<DynamicsPage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: false,
|
elevation: 0,
|
||||||
title: const Text('动态'),
|
scrolledUnderElevation: 0,
|
||||||
actions: [
|
titleSpacing: 0,
|
||||||
|
title: SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
Obx(
|
Obx(
|
||||||
() => PopupMenuButton(
|
() => SegmentedButton<DynamicsType>(
|
||||||
initialValue: _dynamicsController.dynamicsType.value,
|
showSelectedIcon: false,
|
||||||
position: PopupMenuPosition.under,
|
style: ButtonStyle(
|
||||||
itemBuilder: (context) => [
|
padding: MaterialStateProperty.all(
|
||||||
for (var i in DynamicsType.values) ...[
|
const EdgeInsets.symmetric(
|
||||||
PopupMenuItem(
|
vertical: 0, horizontal: 10)),
|
||||||
value: i.values,
|
side: MaterialStateProperty.all(
|
||||||
onTap: () =>
|
BorderSide(
|
||||||
_dynamicsController.onSelectType(i.values, i.labels),
|
color: Theme.of(context).hintColor, width: 0.5),
|
||||||
child: Text(i.labels),
|
),
|
||||||
|
),
|
||||||
|
segments: <ButtonSegment<DynamicsType>>[
|
||||||
|
for (var i in _dynamicsController.filterTypeList) ...[
|
||||||
|
ButtonSegment<DynamicsType>(
|
||||||
|
value: i['value'],
|
||||||
|
label: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text(i['label']),
|
||||||
|
),
|
||||||
|
enabled: i['enabled'],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
selected: <DynamicsType>{
|
||||||
|
_dynamicsController.dynamicsType.value
|
||||||
|
},
|
||||||
|
onSelectionChanged: (Set<DynamicsType> newSelection) {
|
||||||
|
_dynamicsController.dynamicsType.value =
|
||||||
|
newSelection.first;
|
||||||
|
_dynamicsController.onSelectType(newSelection.first);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 10,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
_dynamicsController.mid = -1;
|
||||||
|
_dynamicsController.dynamicsType.value =
|
||||||
|
DynamicsType.values[0];
|
||||||
|
SmartDialog.showToast('还原默认加载',
|
||||||
|
alignment: Alignment.topCenter);
|
||||||
|
_dynamicsController.queryFollowDynamic();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.history),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
],
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_dynamicsController.dynamicsTypeLabel.value,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
|
_dynamicsController.page = 1;
|
||||||
|
_dynamicsController.queryFollowUp();
|
||||||
await _dynamicsController.queryFollowDynamic();
|
await _dynamicsController.queryFollowDynamic();
|
||||||
},
|
},
|
||||||
child: FutureBuilder(
|
child: CustomScrollView(
|
||||||
|
controller: _dynamicsController.scrollController,
|
||||||
|
slivers: [
|
||||||
|
FutureBuilder(
|
||||||
|
future: _dynamicsController.queryFollowUp(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
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: 115,
|
||||||
|
child: UpPanelSkeleton(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
future: _futureBuilderFuture,
|
future: _futureBuilderFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
Map data = snapshot.data;
|
Map data = snapshot.data;
|
||||||
if (data['status']) {
|
if (data['status']) {
|
||||||
List<DynamicItemModel> list = _dynamicsController.dynamicsList!;
|
List<DynamicItemModel> list =
|
||||||
|
_dynamicsController.dynamicsList!;
|
||||||
return Obx(
|
return Obx(
|
||||||
() => ListView.builder(
|
() => SliverList(
|
||||||
controller: _dynamicsController.scrollController,
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: list.length,
|
|
||||||
itemBuilder: (BuildContext context, index) {
|
|
||||||
return DynamicPanel(item: list[index]);
|
return DynamicPanel(item: list[index]);
|
||||||
},
|
}, childCount: list.length),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return CustomScrollView(
|
return HttpError(
|
||||||
slivers: [
|
|
||||||
HttpError(
|
|
||||||
errMsg: data['msg'],
|
errMsg: data['msg'],
|
||||||
fn: () => _dynamicsController.queryFollowDynamic(),
|
fn: () => _dynamicsController.queryFollowDynamic(),
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 骨架屏
|
// 骨架屏
|
||||||
return ListView.builder(
|
return skeleton();
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: 5,
|
|
||||||
itemBuilder: ((context, index) => const DynamicCardSkeleton()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget skeleton() {
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
|
return const DynamicCardSkeleton();
|
||||||
|
}, childCount: 5),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
263
lib/pages/dynamics/widgets/up_panel.dart
Normal file
263
lib/pages/dynamics/widgets/up_panel.dart
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_meedu_media_kit/meedu_player.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/common/widgets/badge.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/models/common/dynamics_type.dart';
|
||||||
|
import 'package:pilipala/models/dynamics/up.dart';
|
||||||
|
import 'package:pilipala/pages/dynamics/controller.dart';
|
||||||
|
|
||||||
|
class UpPanel extends StatefulWidget {
|
||||||
|
FollowUpModel? upData;
|
||||||
|
UpPanel(this.upData, {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);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
upList = widget.upData!.upList!;
|
||||||
|
liveList = widget.upData!.liveUsers!.items!;
|
||||||
|
upList.insert(0, UpItem(face: '', uname: '全部动态', mid: -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverPersistentHeader(
|
||||||
|
floating: true,
|
||||||
|
pinned: false,
|
||||||
|
delegate: _SliverHeaderDelegate(
|
||||||
|
height: 115,
|
||||||
|
child: Container(
|
||||||
|
height: 115,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 5, left: 12, right: 12, bottom: 5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'最常访问',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
controller: scrollController,
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
for (int i = 0; i < liveList.length; i++) ...[
|
||||||
|
upItemBuild(liveList[i], i)
|
||||||
|
],
|
||||||
|
VerticalDivider(
|
||||||
|
indent: 15,
|
||||||
|
endIndent: 35,
|
||||||
|
width: 26,
|
||||||
|
color: Theme.of(context).primaryColor.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
for (int i = 0; i < upList.length; i++) ...[
|
||||||
|
upItemBuild(upList[i], i)
|
||||||
|
],
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget upItemBuild(data, i) {
|
||||||
|
bool isCurrent = currentMid == data.mid || currentMid == -1;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (data.type == 'up') {
|
||||||
|
currentMid = data.mid;
|
||||||
|
Get.find<DynamicsController>().mid = data.mid;
|
||||||
|
Get.find<DynamicsController>().onSelectUp(data.mid);
|
||||||
|
int liveLen = liveList.length;
|
||||||
|
int upLen = upList.length;
|
||||||
|
double itemWidth = contentWidth + itemPadding.horizontal;
|
||||||
|
double screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
double moveDistance = 0.0;
|
||||||
|
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||||
|
moveDistance =
|
||||||
|
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||||
|
} else {
|
||||||
|
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
|
||||||
|
}
|
||||||
|
scrollController.animateTo(
|
||||||
|
moveDistance,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
} else if (data.type == 'live') {
|
||||||
|
SmartDialog.showToast('直播功能暂未开发');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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.onPrimary,
|
||||||
|
alignment: AlignmentDirectional.bottomCenter,
|
||||||
|
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||||
|
isLabelVisible: data.type == 'live' ||
|
||||||
|
(data.type == 'up' && (data.hasUpdate ?? false)),
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
width: 49,
|
||||||
|
height: 49,
|
||||||
|
src: data.face,
|
||||||
|
type: 'avatar',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: SizedBox(
|
||||||
|
width: contentWidth,
|
||||||
|
child: Text(
|
||||||
|
data.uname,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
_SliverHeaderDelegate({required this.height, required this.child});
|
||||||
|
|
||||||
|
final double height;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get maxExtent => height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minExtent => height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpPanelSkeleton extends StatelessWidget {
|
||||||
|
const UpPanelSkeleton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(top: 5, left: 12, right: 12, bottom: 5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'最常访问',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: 10,
|
||||||
|
itemBuilder: ((context, index) => Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 49,
|
||||||
|
height: 49,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 6),
|
||||||
|
width: 45,
|
||||||
|
height: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user