mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: live search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
lib/models/common/live_search_type.dart
Normal file
1
lib/models/common/live_search_type.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum LiveSearchType { room, user }
|
||||
39
lib/models/live/live_search/data.dart
Normal file
39
lib/models/live/live_search/data.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
19
lib/models/live/live_search/live_search.dart
Normal file
19
lib/models/live/live_search/live_search.dart
Normal 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>),
|
||||
);
|
||||
}
|
||||
18
lib/models/live/live_search/room.dart
Normal file
18
lib/models/live/live_search/room.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
38
lib/models/live/live_search/room_item.dart
Normal file
38
lib/models/live/live_search/room_item.dart
Normal 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>),
|
||||
);
|
||||
}
|
||||
17
lib/models/live/live_search/user.dart
Normal file
17
lib/models/live/live_search/user.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
33
lib/models/live/live_search/user_item.dart
Normal file
33
lib/models/live/live_search/user_item.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
26
lib/models/live/live_search/watched_show.dart
Normal file
26
lib/models/live/live_search/watched_show.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
55
lib/pages/live_search/child/controller.dart
Normal file
55
lib/pages/live_search/child/controller.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
151
lib/pages/live_search/child/view.dart
Normal file
151
lib/pages/live_search/child/view.dart
Normal 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;
|
||||
}
|
||||
63
lib/pages/live_search/controller.dart
Normal file
63
lib/pages/live_search/controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
103
lib/pages/live_search/view.dart
Normal file
103
lib/pages/live_search/view.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
114
lib/pages/live_search/widgets/live_search_room.dart
Normal file
114
lib/pages/live_search/widgets/live_search_room.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/pages/live_search/widgets/live_search_user.dart
Normal file
66
lib/pages/live_search/widgets/live_search_user.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user