feat: live search

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-06 20:31:09 +08:00
parent 867efecc54
commit 661e7bfa78
23 changed files with 835 additions and 68 deletions

View File

@@ -819,4 +819,7 @@ class Api {
static const String setLiveFavTag =
'${HttpString.liveBaseUrl}/xlive/app-interface/v2/second/set_fav_tag';
static const String liveSearch =
'${HttpString.liveBaseUrl}/xlive/app-interface/v2/search_live';
}

View File

@@ -2,6 +2,7 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/live_search_type.dart';
import 'package:PiliPlus/models/live/live_area_list/area_item.dart';
import 'package:PiliPlus/models/live/live_area_list/area_list.dart';
import 'package:PiliPlus/models/live/live_emoticons/data.dart';
@@ -12,6 +13,7 @@ import 'package:PiliPlus/models/live/live_room/danmu_info.dart';
import 'package:PiliPlus/models/live/live_room/item.dart';
import 'package:PiliPlus/models/live/live_room/room_info.dart';
import 'package:PiliPlus/models/live/live_room/room_info_h5.dart';
import 'package:PiliPlus/models/live/live_search/data.dart';
import 'package:PiliPlus/models/live/live_second_list/data.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
@@ -432,4 +434,44 @@ class LiveHttp {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState<LiveSearchData>> liveSearch({
required bool isLogin,
required int page,
required String keyword,
required LiveSearchType type,
}) async {
final params = {
if (isLogin) 'access_key': Accounts.main.accessKey,
'appkey': Constants.appKey,
'actionKey': 'appkey',
'build': '8350200',
'c_locale': 'zh_CN',
'device': 'pad',
'page': page,
'pagesize': 30,
'keyword': keyword,
'disable_rcmd': '0',
'mobi_app': 'android_hd',
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'type': type.name,
};
Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request().get(
Api.liveSearch,
queryParameters: params,
);
if (res.data['code'] == 0) {
return LoadingState.success(LiveSearchData.fromJson(res.data['data']));
} else {
return LoadingState.error(res.data['message']);
}
}
}

View File

@@ -0,0 +1 @@
enum LiveSearchType { room, user }

View File

@@ -0,0 +1,39 @@
import 'room.dart';
import 'user.dart';
class LiveSearchData {
String? type;
int? page;
int? pagesize;
Room? room;
User? user;
String? trackId;
String? abtestId;
String? query;
LiveSearchData({
this.type,
this.page,
this.pagesize,
this.room,
this.user,
this.trackId,
this.abtestId,
this.query,
});
factory LiveSearchData.fromJson(Map<String, dynamic> json) => LiveSearchData(
type: json['type'] as String?,
page: json['page'] as int?,
pagesize: json['pagesize'] as int?,
room: json['room'] == null
? null
: Room.fromJson(json['room'] as Map<String, dynamic>),
user: json['user'] == null
? null
: User.fromJson(json['user'] as Map<String, dynamic>),
trackId: json['track_id'] as String?,
abtestId: json['abtest_id'] as String?,
query: json['query'] as String?,
);
}

View File

@@ -0,0 +1,19 @@
import 'data.dart';
class LiveSearch {
int? code;
String? message;
int? ttl;
LiveSearchData? data;
LiveSearch({this.code, this.message, this.ttl, this.data});
factory LiveSearch.fromJson(Map<String, dynamic> json) => LiveSearch(
code: json['code'] as int?,
message: json['message'] as String?,
ttl: json['ttl'] as int?,
data: json['data'] == null
? null
: LiveSearchData.fromJson(json['data'] as Map<String, dynamic>),
);
}

View File

@@ -0,0 +1,18 @@
import 'room_item.dart';
class Room {
List<LiveSearchRoomItemModel>? list;
int? totalRoom;
int? totalPage;
Room({this.list, this.totalRoom, this.totalPage});
factory Room.fromJson(Map<String, dynamic> json) => Room(
list: (json['list'] as List<dynamic>?)
?.map((e) =>
LiveSearchRoomItemModel.fromJson(e as Map<String, dynamic>))
.toList(),
totalRoom: json['total_room'] as int?,
totalPage: json['total_page'] as int?,
);
}

View File

@@ -0,0 +1,38 @@
import 'watched_show.dart';
class LiveSearchRoomItemModel {
int? roomid;
String? cover;
String? title;
String? name;
String? face;
int? online;
String? link;
WatchedShow? watchedShow;
LiveSearchRoomItemModel({
this.roomid,
this.cover,
this.title,
this.name,
this.face,
this.online,
this.link,
this.watchedShow,
});
factory LiveSearchRoomItemModel.fromJson(Map<String, dynamic> json) =>
LiveSearchRoomItemModel(
roomid: json['roomid'] as int?,
cover: json['cover'] as String?,
title: json['title'] as String?,
name: json['name'] as String?,
face: json['face'] as String?,
online: json['online'] as int?,
link: json['link'] as String?,
watchedShow: json['watched_show'] == null
? null
: WatchedShow.fromJson(
json['watched_show'] as Map<String, dynamic>),
);
}

View File

@@ -0,0 +1,17 @@
import 'package:PiliPlus/models/live/live_search/user_item.dart';
class User {
List<LiveSearchUserItemModel>? list;
int? totalUser;
int? totalPage;
User({this.list, this.totalUser, this.totalPage});
factory User.fromJson(Map<String, dynamic> json) => User(
list: (json['list'] as List<dynamic>?)
?.map((e) => LiveSearchUserItemModel.fromJson(e))
.toList(),
totalUser: json['total_user'] as int?,
totalPage: json['total_page'] as int?,
);
}

View File

@@ -0,0 +1,33 @@
class LiveSearchUserItemModel {
String? face;
String? name;
int? liveStatus;
String? areaName;
int? fansNum;
List? roomTags;
int? roomid;
String? link;
LiveSearchUserItemModel({
this.face,
this.name,
this.liveStatus,
this.areaName,
this.fansNum,
this.roomTags,
this.roomid,
this.link,
});
factory LiveSearchUserItemModel.fromJson(Map<String, dynamic> json) =>
LiveSearchUserItemModel(
face: json['face'] as String?,
name: json['name'] as String?,
liveStatus: json['live_status'] as int?,
areaName: json['areaName'] as String?,
fansNum: json['fansNum'] as int?,
roomTags: json['roomTags'],
roomid: json['roomid'] as int?,
link: json['link'] as String?,
);
}

View File

@@ -0,0 +1,26 @@
class WatchedShow {
int? num;
String? textSmall;
String? textLarge;
String? icon;
int? iconLocation;
String? iconWeb;
WatchedShow({
this.num,
this.textSmall,
this.textLarge,
this.icon,
this.iconLocation,
this.iconWeb,
});
factory WatchedShow.fromJson(Map<String, dynamic> json) => WatchedShow(
num: json['num'] as int?,
textSmall: json['text_small'] as String?,
textLarge: json['text_large'] as String?,
icon: json['icon'] as String?,
iconLocation: json['icon_location'] as int?,
iconWeb: json['icon_web'] as String?,
);
}

View File

@@ -45,22 +45,9 @@ class BangumiCardVMemberHome extends StatelessWidget {
);
}),
),
bangumiContent(bangumiItem)
],
),
),
);
}
}
Widget bangumiContent(SpaceArchiveItem bangumiItem) {
return Expanded(
child: Padding(
Padding(
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
child: Text(
bangumiItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
@@ -69,9 +56,10 @@ Widget bangumiContent(SpaceArchiveItem bangumiItem) {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
),
],
),
),
);
}
}

View File

@@ -52,21 +52,9 @@ class BangumiCardVSearch extends StatelessWidget {
);
}),
),
bagumiContent(context)
],
),
),
);
}
Widget bagumiContent(context) {
return Expanded(
child: Padding(
Padding(
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
child: Text(
item.title!.map((e) => e['text']).join(),
textAlign: TextAlign.start,
style: const TextStyle(
@@ -75,6 +63,7 @@ class BangumiCardVSearch extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
)
],
),
),

View File

@@ -37,6 +37,7 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
@override
Widget build(BuildContext context) {
super.build(context);
final ThemeData theme = Theme.of(context);
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
@@ -58,8 +59,8 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: SliverMainAxisGroup(slivers: [
Obx(() => _buildTop(controller.topState.value)),
Obx(() => _buildBody(controller.loadingState.value)),
Obx(() => _buildTop(theme, controller.topState.value)),
Obx(() => _buildBody(theme, controller.loadingState.value)),
]),
),
],
@@ -68,11 +69,11 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
);
}
Widget _buildTop(Pair<LiveCardList?, LiveCardList?> data) {
Widget _buildTop(ThemeData theme, Pair<LiveCardList?, LiveCardList?> data) {
return SliverMainAxisGroup(
slivers: [
if (data.first != null)
SliverToBoxAdapter(child: _buildFollowList(data.first!)),
SliverToBoxAdapter(child: _buildFollowList(theme, data.first!)),
if (data.second?.cardData?.areaEntranceV3?.list?.isNotEmpty == true)
SliverToBoxAdapter(
child: Row(
@@ -94,12 +95,10 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
),
text: index == 0 ? '推荐' : '${item.title}',
bgColor: index == controller.areaIndex.value
? Theme.of(context).colorScheme.secondaryContainer
? theme.colorScheme.secondaryContainer
: Colors.transparent,
textColor: index == controller.areaIndex.value
? Theme.of(context)
.colorScheme
.onSecondaryContainer
? theme.colorScheme.onSecondaryContainer
: null,
onTap: (value) {
controller.onSelectArea(
@@ -133,7 +132,7 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
);
}
Widget _buildBody(LoadingState<List?> loadingState) {
Widget _buildBody(ThemeData theme, LoadingState<List?> loadingState) {
return switch (loadingState) {
Loading() => SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
@@ -169,14 +168,10 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
),
text: '${item.name}',
bgColor: index == controller.tagIndex.value
? Theme.of(context)
.colorScheme
.secondaryContainer
? theme.colorScheme.secondaryContainer
: Colors.transparent,
textColor: index == controller.tagIndex.value
? Theme.of(context)
.colorScheme
.onSecondaryContainer
? theme.colorScheme.onSecondaryContainer
: null,
onTap: (value) {
controller.onSelectTag(
@@ -224,8 +219,7 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
};
}
Widget _buildFollowList(LiveCardList item) {
final theme = Theme.of(context);
Widget _buildFollowList(ThemeData theme, LiveCardList item) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -316,8 +316,8 @@ class _LiveAreaPageState extends State<LiveAreaPage> {
border: Border.all(
color: theme.colorScheme.outline,
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: theme.colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
child: SearchText(
text: item.name!,

View File

@@ -3,6 +3,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/live/live_area_list/area_item.dart';
import 'package:PiliPlus/pages/live_area_detail/child/view.dart';
import 'package:PiliPlus/pages/live_area_detail/controller.dart';
import 'package:PiliPlus/pages/live_search/view.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -35,7 +36,7 @@ class _LiveAreaDetailPageState extends State<LiveAreaDetailPage> {
actions: [
IconButton(
onPressed: () {
// TODO: search
Get.to(const LiveSearchPage());
},
icon: const Icon(Icons.search),
),

View File

@@ -0,0 +1,55 @@
import 'package:PiliPlus/http/live.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/live_search_type.dart';
import 'package:PiliPlus/models/live/live_search/data.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/pages/live_search/controller.dart';
import 'package:PiliPlus/utils/storage.dart';
class LiveSearchChildController
extends CommonListController<LiveSearchData, dynamic> {
LiveSearchChildController(this.controller, this.searchType);
final isLogin = Accounts.main.isLogin;
final LiveSearchController controller;
final LiveSearchType searchType;
@override
void checkIsEnd(int length) {
switch (searchType) {
case LiveSearchType.room:
if (controller.counts.first != -1 &&
length >= controller.counts.first) {
isEnd = true;
}
break;
case LiveSearchType.user:
if (controller.counts[1] != -1 && length >= controller.counts[1]) {
isEnd = true;
}
break;
}
}
@override
List? getDataList(response) {
switch (searchType) {
case LiveSearchType.room:
controller.counts[searchType.index] = response.room?.totalRoom ?? 0;
return response.room?.list;
case LiveSearchType.user:
controller.counts[searchType.index] = response.user?.totalUser ?? 0;
return response.user?.list;
}
}
@override
Future<LoadingState<LiveSearchData>> customGetData() {
return LiveHttp.liveSearch(
isLogin: isLogin,
page: currentPage,
keyword: controller.editingController.text,
type: searchType,
);
}
}

View File

@@ -0,0 +1,151 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart';
import 'package:PiliPlus/common/skeleton/video_card_v.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/live_search_type.dart';
import 'package:PiliPlus/pages/live_search/child/controller.dart';
import 'package:PiliPlus/pages/live_search/widgets/live_search_room.dart';
import 'package:PiliPlus/pages/live_search/widgets/live_search_user.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LiveSearchChildPage extends StatefulWidget {
const LiveSearchChildPage({
super.key,
required this.controller,
required this.searchType,
});
final LiveSearchChildController controller;
final LiveSearchType searchType;
@override
State<LiveSearchChildPage> createState() => _LiveSearchChildPageState();
}
class _LiveSearchChildPageState extends State<LiveSearchChildPage>
with AutomaticKeepAliveClientMixin {
late final bool dynamicsWaterfallFlow = GStorage.setting
.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);
LiveSearchChildController get _controller => widget.controller;
@override
Widget build(BuildContext context) {
super.build(context);
double padding = widget.searchType == LiveSearchType.room ? 12 : 0;
return refreshIndicator(
onRefresh: _controller.onRefresh,
child: CustomScrollView(
controller: _controller.scrollController,
slivers: [
SliverPadding(
padding: EdgeInsets.only(
top: padding,
left: padding,
right: padding,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
),
],
),
);
}
Widget get _buildLoading {
return switch (widget.searchType) {
LiveSearchType.room => SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.smallCardWidth,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent: MediaQuery.textScalerOf(context).scale(90),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return const VideoCardVSkeleton();
},
childCount: 10,
),
),
LiveSearchType.user => SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
mainAxisExtent: 60,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return const MsgFeedTopSkeleton();
},
childCount: 12,
),
),
};
}
Widget _buildBody(LoadingState<List?> loadingState) {
return switch (loadingState) {
Loading() => _buildLoading,
Success() => loadingState.response?.isNotEmpty == true
? Builder(
builder: (context) {
return switch (widget.searchType) {
LiveSearchType.room => SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.smallCardWidth,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(60),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == loadingState.response!.length - 1) {
_controller.onLoadMore();
}
return LiveCardVSearch(
item: loadingState.response![index],
);
},
childCount: loadingState.response!.length,
),
),
LiveSearchType.user => SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
mainAxisExtent: 60,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index == loadingState.response!.length - 1) {
_controller.onLoadMore();
}
return LiveSearchUserItem(
item: loadingState.response![index],
);
},
childCount: loadingState.response!.length,
),
),
};
},
)
: HttpError(
onReload: _controller.onReload,
),
Error() => HttpError(
errMsg: loadingState.errMsg,
onReload: _controller.onReload,
),
};
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -0,0 +1,63 @@
import 'package:PiliPlus/models/common/live_search_type.dart';
import 'package:PiliPlus/pages/live_search/child/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LiveSearchController extends GetxController
with GetSingleTickerProviderStateMixin {
late final tabController = TabController(vsync: this, length: 2);
final editingController = TextEditingController();
final focusNode = FocusNode();
final mid = Get.parameters['mid'];
final uname = Get.parameters['uname'];
final RxBool hasData = false.obs;
final RxList<int> counts = <int>[-1, -1].obs;
late final roomCtr = Get.put(
LiveSearchChildController(this, LiveSearchType.room),
tag: Utils.generateRandomString(8));
late final userCtr = Get.put(
LiveSearchChildController(this, LiveSearchType.user),
tag: Utils.generateRandomString(8));
void onClear() {
if (editingController.value.text.isNotEmpty) {
editingController.clear();
counts.value = <int>[-1, -1];
hasData.value = false;
focusNode.requestFocus();
} else {
Get.back();
}
}
late final regex = RegExp(r'^\d+$');
void submit() {
if (editingController.text.isNotEmpty) {
if (regex.hasMatch(editingController.text)) {
Get.toNamed('/liveRoom?roomid=${editingController.text}');
} else {
hasData.value = true;
roomCtr
..scrollController.jumpToTop()
..onReload();
userCtr
..scrollController.jumpToTop()
..onReload();
}
}
}
@override
void onClose() {
editingController.dispose();
focusNode.dispose();
tabController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,103 @@
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/models/common/live_search_type.dart';
import 'package:PiliPlus/pages/live_search/child/view.dart';
import 'package:PiliPlus/pages/live_search/controller.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LiveSearchPage extends StatefulWidget {
const LiveSearchPage({super.key});
@override
State<LiveSearchPage> createState() => _LiveSearchPageState();
}
class _LiveSearchPageState extends State<LiveSearchPage> {
final _controller =
Get.put(LiveSearchController(), tag: Utils.generateRandomString(8));
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
actions: [
IconButton(
tooltip: '搜索',
onPressed: _controller.submit,
icon: const Icon(Icons.search, size: 22),
),
const SizedBox(width: 10)
],
title: TextField(
autofocus: true,
focusNode: _controller.focusNode,
controller: _controller.editingController,
textInputAction: TextInputAction.search,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
hintText: '搜索房间或主播',
border: InputBorder.none,
suffixIcon: IconButton(
tooltip: '清空',
icon: const Icon(Icons.clear, size: 22),
onPressed: _controller.onClear,
),
),
onSubmitted: (value) => _controller.submit(),
onChanged: (value) {
if (value.isEmpty) {
_controller.hasData.value = false;
}
},
),
),
body: SafeArea(
top: false,
bottom: false,
child: Obx(() {
return Opacity(
opacity: _controller.hasData.value ? 1 : 0,
child: Column(
children: [
TabBar(
controller: _controller.tabController,
tabs: [
Obx(
() => Tab(
text:
'正在直播 ${_controller.counts[0] != -1 ? _controller.counts[0] : ''}',
),
),
Obx(
() => Tab(
text:
'主播 ${_controller.counts[1] != -1 ? _controller.counts[1] : ''}',
),
),
],
),
Expanded(
child: tabBarView(
controller: _controller.tabController,
children: [
LiveSearchChildPage(
controller: _controller.roomCtr,
searchType: LiveSearchType.room,
),
LiveSearchChildPage(
controller: _controller.userCtr,
searchType: LiveSearchType.user,
),
],
),
),
],
),
);
}),
),
);
}
}

View File

@@ -0,0 +1,114 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/image/image_save.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/live/live_search/room_item.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// 视频卡片 - 垂直布局
class LiveCardVSearch extends StatelessWidget {
final LiveSearchRoomItemModel item;
const LiveCardVSearch({
super.key,
required this.item,
});
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(item.roomid);
return Card(
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {
Get.toNamed('/liveRoom?roomid=${item.roomid}');
},
onLongPress: () => imageSaveDialog(
title: item.title,
cover: item.cover,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
clipBehavior: Clip.none,
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: item.cover!,
width: maxWidth,
height: maxHeight,
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: videoStat(context),
),
),
],
);
}),
),
Padding(
padding: const EdgeInsets.fromLTRB(5, 8, 5, 4),
child: Text(
'${item.title}',
textAlign: TextAlign.start,
style: const TextStyle(
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
Widget videoStat(context) {
return Container(
height: 50,
padding: const EdgeInsets.only(top: 26, left: 10, right: 10),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${item.name}',
style: const TextStyle(fontSize: 11, color: Colors.white),
),
if (item.watchedShow?.textSmall != null)
Text(
'${Utils.numFormat(item.watchedShow!.textSmall)}围观',
style: const TextStyle(fontSize: 11, color: Colors.white),
),
],
),
);
}
}

View File

@@ -0,0 +1,66 @@
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/live/live_search/user_item.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LiveSearchUserItem extends StatelessWidget {
const LiveSearchUserItem({
super.key,
required this.item,
});
final LiveSearchUserItemModel item;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
);
return InkWell(
onTap: () => Get.toNamed(
'/liveRoom?roomid=${item.roomid}',
),
child: Row(
children: [
const SizedBox(width: 15),
NetworkImgLayer(
src: item.face,
width: 42,
height: 42,
type: 'avatar',
),
const SizedBox(width: 10),
Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Text(
item.name!,
style: const TextStyle(
fontSize: 14,
),
),
if (item.liveStatus == 1) ...[
const SizedBox(width: 10),
Image.asset(height: 14, 'assets/images/live/live.gif'),
],
],
),
const SizedBox(height: 2),
Text(
'分区: ${item.areaName ?? ''} 关注数: ${Utils.numFormat(item.fansNum ?? 0)}',
style: style,
),
],
)
],
),
);
}
}

View File

@@ -50,7 +50,9 @@ class _MemberArticleState extends State<MemberArticle>
slivers: [
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80),
top: 7,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: SliverGrid(
gridDelegate: Grid.videoCardHDelegate(context),
delegate: SliverChildBuilderDelegate(

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/models/common/member/search_type.dart';
import 'package:PiliPlus/pages/member_search/child/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -37,8 +38,12 @@ class MemberSearchController extends GetxController
void submit() {
if (editingController.text.isNotEmpty) {
hasData.value = true;
arcCtr.onReload();
dynCtr.onReload();
arcCtr
..scrollController.jumpToTop()
..onReload();
dynCtr
..scrollController.jumpToTop()
..onReload();
}
}