mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
refactor: rcmd hot
This commit is contained in:
@@ -132,57 +132,51 @@ class VideoCardV extends StatelessWidget {
|
||||
label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
if (longPress != null) {
|
||||
longPress!();
|
||||
}
|
||||
},
|
||||
// onLongPressEnd: (details) {
|
||||
// if (longPressEnd != null) {
|
||||
// longPressEnd!();
|
||||
// }
|
||||
// },
|
||||
child: InkWell(
|
||||
onTap: () async => onPushDetail(heroTag),
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
if (videoItem.duration > 0)
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: 'small',
|
||||
type: 'gray',
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
// semanticsLabel:
|
||||
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
],
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () async => onPushDetail(heroTag),
|
||||
onLongPress: () {
|
||||
if (longPress != null) {
|
||||
longPress!();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
if (videoItem.duration > 0)
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: 'small',
|
||||
type: 'gray',
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
// semanticsLabel:
|
||||
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)),
|
||||
VideoContent(videoItem: videoItem)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av')
|
||||
Positioned(
|
||||
|
||||
26
lib/http/loading_state.dart
Normal file
26
lib/http/loading_state.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
abstract class LoadingState<T> {
|
||||
const LoadingState();
|
||||
|
||||
factory LoadingState.loading() = Loading;
|
||||
factory LoadingState.empty() = Empty;
|
||||
factory LoadingState.success(T response) = Success<T>;
|
||||
factory LoadingState.error(String errMsg) = Error;
|
||||
}
|
||||
|
||||
class Loading extends LoadingState<Never> {
|
||||
const Loading();
|
||||
}
|
||||
|
||||
class Empty extends LoadingState<Never> {
|
||||
const Empty();
|
||||
}
|
||||
|
||||
class Success<T> extends LoadingState<T> {
|
||||
final T response;
|
||||
const Success(this.response);
|
||||
}
|
||||
|
||||
class Error extends LoadingState<Never> {
|
||||
final String errMsg;
|
||||
const Error(this.errMsg);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:developer';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import '../common/constants.dart';
|
||||
@@ -34,7 +34,8 @@ class VideoHttp {
|
||||
static Box userInfoCache = GStorage.userInfo;
|
||||
|
||||
// 首页推荐视频
|
||||
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
|
||||
static Future<LoadingState> rcmdVideoList(
|
||||
{required int ps, required int freshIdx}) async {
|
||||
var res = await Request().get(
|
||||
Api.recommendListWeb,
|
||||
data: {
|
||||
@@ -49,10 +50,8 @@ class VideoHttp {
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<RecVideoItemModel> list = [];
|
||||
List<int> blackMidsList = localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||
.map<int>((e) => e as int)
|
||||
.toList();
|
||||
List<int> blackMidsList =
|
||||
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
for (var i in res.data['data']['item']) {
|
||||
//过滤掉live与ad,以及拉黑用户
|
||||
if (i['goto'] == 'av' &&
|
||||
@@ -64,14 +63,18 @@ class VideoHttp {
|
||||
}
|
||||
}
|
||||
}
|
||||
return {'status': true, 'data': list};
|
||||
if (list.isNotEmpty) {
|
||||
return LoadingState.success(list);
|
||||
} else {
|
||||
return LoadingState.empty();
|
||||
}
|
||||
} else {
|
||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加额外的loginState变量模拟未登录状态
|
||||
static Future rcmdVideoListApp(
|
||||
static Future<LoadingState> rcmdVideoListApp(
|
||||
{bool loginStatus = true, required int freshIdx}) async {
|
||||
var data = {
|
||||
'access_key': loginStatus
|
||||
@@ -138,10 +141,8 @@ class VideoHttp {
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<RecVideoItemAppModel> list = [];
|
||||
List<int> blackMidsList = localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||
.map<int>((e) => e as int)
|
||||
.toList();
|
||||
List<int> blackMidsList =
|
||||
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||
for (var i in res.data['data']['items']) {
|
||||
// 屏蔽推广和拉黑用户
|
||||
if (i['card_goto'] != 'ad_av' &&
|
||||
@@ -156,36 +157,41 @@ class VideoHttp {
|
||||
}
|
||||
}
|
||||
}
|
||||
return {'status': true, 'data': list};
|
||||
if (list.isNotEmpty) {
|
||||
return LoadingState.success(list);
|
||||
} else {
|
||||
return LoadingState.empty();
|
||||
}
|
||||
} else {
|
||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// 最热视频
|
||||
static Future hotVideoList({required int pn, required int ps}) async {
|
||||
try {
|
||||
var res = await Request().get(
|
||||
Api.hotList,
|
||||
data: {'pn': pn, 'ps': ps},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<HotVideoItemModel> list = [];
|
||||
List<int> blackMidsList = localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||
.map<int>((e) => e as int)
|
||||
.toList();
|
||||
for (var i in res.data['data']['list']) {
|
||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
}
|
||||
static Future<LoadingState> hotVideoList(
|
||||
{required int pn, required int ps}) async {
|
||||
var res = await Request().get(
|
||||
Api.hotList,
|
||||
data: {'pn': pn, 'ps': ps},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
List<HotVideoItemModel> list = [];
|
||||
List<int> blackMidsList = localCache
|
||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||
.map<int>((e) => e as int)
|
||||
.toList();
|
||||
for (var i in res.data['data']['list']) {
|
||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
}
|
||||
return {'status': true, 'data': list};
|
||||
} else {
|
||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
||||
}
|
||||
} catch (err) {
|
||||
return {'status': false, 'data': [], 'msg': err};
|
||||
if (list.isNotEmpty) {
|
||||
return LoadingState.success(list);
|
||||
} else {
|
||||
return LoadingState.empty();
|
||||
}
|
||||
} else {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
63
lib/pages/common/common_controller.dart
Normal file
63
lib/pages/common/common_controller.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
abstract class CommonController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
int currentPage = 1;
|
||||
bool isLoading = false;
|
||||
Rx<LoadingState> loadingState = LoadingState.loading().obs;
|
||||
|
||||
Future<LoadingState> customGetData();
|
||||
|
||||
List? handleResponse(List currentList, List dataList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
void handleSuccess(List currentList, List dataList) {}
|
||||
|
||||
Future queryData([bool isRefresh = true]) async {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
LoadingState response = await customGetData();
|
||||
if (response is Success) {
|
||||
currentPage++;
|
||||
List currentList = loadingState.value is Success
|
||||
? (loadingState.value as Success).response
|
||||
: [];
|
||||
List? handleList = handleResponse(currentList, response.response);
|
||||
loadingState.value = isRefresh
|
||||
? handleList != null
|
||||
? LoadingState.success(handleList)
|
||||
: response
|
||||
: LoadingState.success(currentList + response.response);
|
||||
handleSuccess(currentList, response.response);
|
||||
} else {
|
||||
if (isRefresh) {
|
||||
loadingState.value = response;
|
||||
}
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
Future onRefresh() async {
|
||||
currentPage = 1;
|
||||
await queryData();
|
||||
}
|
||||
|
||||
Future onLoadMore() async {
|
||||
await queryData(false);
|
||||
}
|
||||
|
||||
void animateToTop() {
|
||||
scrollController.animToTop();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
scrollController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,21 @@
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/pages/common/common_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
import 'package:PiliPalaX/models/model_hot_video_item.dart';
|
||||
|
||||
class HotController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
class HotController extends CommonController {
|
||||
final int _count = 20;
|
||||
int _currentPage = 1;
|
||||
RxList<HotVideoItemModel> videoList = <HotVideoItemModel>[].obs;
|
||||
bool isLoadingMore = false;
|
||||
bool flag = false;
|
||||
List<OverlayEntry?> popupDialog = <OverlayEntry?>[];
|
||||
|
||||
// 获取推荐
|
||||
Future queryHotFeed(type) async {
|
||||
if (type != 'onLoad') {
|
||||
_currentPage = 1;
|
||||
}
|
||||
var res = await VideoHttp.hotVideoList(
|
||||
pn: _currentPage,
|
||||
ps: _count,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
videoList.value = res['data'];
|
||||
} else if (type == 'onRefresh') {
|
||||
// videoList.insertAll(0, res['data']);
|
||||
videoList.value = res['data'];
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(res['data']);
|
||||
}
|
||||
_currentPage += 1;
|
||||
if (_currentPage == 2) queryHotFeed('onLoad');
|
||||
}
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
queryData();
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
Future onRefresh() async {
|
||||
await queryHotFeed('onRefresh');
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
await queryHotFeed('onLoad');
|
||||
}
|
||||
|
||||
// 返回顶部
|
||||
void animateToTop() {
|
||||
scrollController.animToTop();
|
||||
}
|
||||
@override
|
||||
Future<LoadingState> customGetData() => VideoHttp.hotVideoList(
|
||||
pn: currentPage,
|
||||
ps: _count,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -23,8 +24,6 @@ class HotPage extends StatefulWidget {
|
||||
|
||||
class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
final HotController _hotController = Get.put(HotController());
|
||||
List videoList = [];
|
||||
Future? _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -32,7 +31,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _hotController.queryHotFeed('init');
|
||||
StreamController<bool> mainStream =
|
||||
Get.find<MainController>().bottomBarStream;
|
||||
StreamController<bool> searchBarStream =
|
||||
@@ -41,10 +39,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
() {
|
||||
if (_hotController.scrollController.position.pixels >=
|
||||
_hotController.scrollController.position.maxScrollExtent - 200) {
|
||||
if (!_hotController.isLoadingMore) {
|
||||
_hotController.isLoadingMore = true;
|
||||
_hotController.onLoad();
|
||||
}
|
||||
_hotController.onLoadMore();
|
||||
}
|
||||
|
||||
final ScrollDirection direction =
|
||||
@@ -63,7 +58,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
void dispose() {
|
||||
_hotController.scrollController.removeListener(() {});
|
||||
_hotController.scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -80,69 +74,29 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
// 单列布局 EdgeInsets.zero
|
||||
padding: const EdgeInsets.fromLTRB(StyleString.safeSpace,
|
||||
StyleString.safeSpace - 5, StyleString.safeSpace, 0),
|
||||
sliver: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
maxCrossAxisExtent: Grid.maxRowWidth * 2,
|
||||
childAspectRatio: StyleString.aspectRatio * 2.4,
|
||||
mainAxisExtent: 0),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return VideoCardH(
|
||||
videoItem: _hotController.videoList[index],
|
||||
showPubdate: true,
|
||||
longPress: () {
|
||||
_hotController.popupDialog.add(_createPopupDialog(
|
||||
_hotController.videoList[index]));
|
||||
Overlay.of(context)
|
||||
.insert(_hotController.popupDialog.last!);
|
||||
},
|
||||
longPressEnd: _removePopupDialog,
|
||||
);
|
||||
}, childCount: _hotController.videoList.length),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () {
|
||||
setState(() {
|
||||
_futureBuilderFuture =
|
||||
_hotController.queryHotFeed('init');
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
mainAxisSpacing: StyleString.cardSpace,
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
maxCrossAxisExtent: Grid.maxRowWidth * 2,
|
||||
childAspectRatio: StyleString.aspectRatio * 2.4),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
}, childCount: 10),
|
||||
);
|
||||
}
|
||||
},
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace,
|
||||
StyleString.safeSpace - 5,
|
||||
StyleString.safeSpace,
|
||||
MediaQuery.of(context).padding.bottom + 10,
|
||||
),
|
||||
sliver: Obx(
|
||||
() => _hotController.loadingState.value is Loading
|
||||
? _buildSkeleton()
|
||||
: _hotController.loadingState.value is Success
|
||||
? _buildBody(_hotController.loadingState.value as Success)
|
||||
: HttpError(
|
||||
errMsg: _hotController.loadingState.value is Error
|
||||
? (_hotController.loadingState.value as Error)
|
||||
.errMsg
|
||||
: '没有相关数据',
|
||||
fn: () {
|
||||
_hotController.loadingState.value =
|
||||
LoadingState.loading();
|
||||
_hotController.onRefresh();
|
||||
}),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 10,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -161,4 +115,48 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkeleton() {
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
mainAxisSpacing: StyleString.cardSpace,
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
maxCrossAxisExtent: Grid.maxRowWidth * 2,
|
||||
childAspectRatio: StyleString.aspectRatio * 2.4,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(Success loadingState) {
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
maxCrossAxisExtent: Grid.maxRowWidth * 2,
|
||||
childAspectRatio: StyleString.aspectRatio * 2.4,
|
||||
mainAxisExtent: 0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return VideoCardH(
|
||||
videoItem: loadingState.response[index],
|
||||
showPubdate: true,
|
||||
longPress: () {
|
||||
_hotController.popupDialog
|
||||
.add(_createPopupDialog(loadingState.response[index]));
|
||||
Overlay.of(context).insert(_hotController.popupDialog.last!);
|
||||
},
|
||||
longPressEnd: _removePopupDialog,
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +1,60 @@
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/models/home/rcmd/result.dart';
|
||||
import 'package:PiliPalaX/models/model_rec_video_item.dart';
|
||||
import 'package:PiliPalaX/pages/common/common_controller.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
|
||||
class RcmdController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
int _currentPage = 0;
|
||||
// RxList<RecVideoItemAppModel> appVideoList = <RecVideoItemAppModel>[].obs;
|
||||
// RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs;
|
||||
List<OverlayEntry?> popupDialog = <OverlayEntry?>[];
|
||||
Box setting = GStorage.setting;
|
||||
RxInt crossAxisCount = 2.obs;
|
||||
class RcmdController extends CommonController {
|
||||
late bool enableSaveLastData;
|
||||
late String defaultRcmdType = 'web';
|
||||
late RxList<dynamic> videoList;
|
||||
List<OverlayEntry?> popupDialog = <OverlayEntry?>[];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
enableSaveLastData =
|
||||
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
|
||||
defaultRcmdType =
|
||||
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
|
||||
if (defaultRcmdType == 'web') {
|
||||
videoList = <RecVideoItemModel>[].obs;
|
||||
} else {
|
||||
videoList = <RecVideoItemAppModel>[].obs;
|
||||
enableSaveLastData = GStorage.setting
|
||||
.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
|
||||
defaultRcmdType = GStorage.setting
|
||||
.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
|
||||
|
||||
currentPage = 0;
|
||||
queryData();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState> customGetData() {
|
||||
return defaultRcmdType == 'app' || defaultRcmdType == 'notLogin'
|
||||
? VideoHttp.rcmdVideoListApp(
|
||||
loginStatus: defaultRcmdType != 'notLogin',
|
||||
freshIdx: currentPage,
|
||||
)
|
||||
: VideoHttp.rcmdVideoList(
|
||||
freshIdx: currentPage,
|
||||
ps: 20,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List? handleResponse(List currentList, List dataList) {
|
||||
return currentPage == 1 && enableSaveLastData
|
||||
? dataList +
|
||||
(currentList.isEmpty ? <RecVideoItemAppModel>[] : currentList)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleSuccess(List currentList, List dataList) {
|
||||
if (dataList.length > 1 && currentList.length < 24) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (currentList.length < 24) queryData(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取推荐
|
||||
Future queryRcmdFeed(type) async {
|
||||
if (type == 'onRefresh') {
|
||||
_currentPage = 0;
|
||||
}
|
||||
late final Map<String, dynamic> res;
|
||||
switch (defaultRcmdType) {
|
||||
case 'app':
|
||||
case 'notLogin':
|
||||
res = await VideoHttp.rcmdVideoListApp(
|
||||
loginStatus: defaultRcmdType != 'notLogin',
|
||||
freshIdx: _currentPage,
|
||||
);
|
||||
break;
|
||||
default: //'web'
|
||||
res = await VideoHttp.rcmdVideoList(
|
||||
freshIdx: _currentPage,
|
||||
ps: 20,
|
||||
);
|
||||
}
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
if (videoList.isNotEmpty) {
|
||||
videoList.addAll(res['data']);
|
||||
} else {
|
||||
videoList.value = res['data'];
|
||||
}
|
||||
} else if (type == 'onRefresh') {
|
||||
if (enableSaveLastData) {
|
||||
videoList.insertAll(0, res['data']);
|
||||
} else {
|
||||
videoList.value = res['data'];
|
||||
}
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(res['data']);
|
||||
}
|
||||
_currentPage += 1;
|
||||
// 若videoList数量太小,可能会影响翻页,此时再次请求
|
||||
// 为避免请求到的数据太少时还在反复请求,要求本次返回数据大于1条才触发
|
||||
if (res['data'].length > 1 && videoList.length < 24) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (videoList.length < 24) queryRcmdFeed('onLoad');
|
||||
});
|
||||
}
|
||||
if (res['data'].length < 5) {
|
||||
SmartDialog.showToast("仅请求到${res['data'].length}条");
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast("${res['msg']},请尝试(重新)登录");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
@override
|
||||
Future onRefresh() async {
|
||||
queryRcmdFeed('onRefresh');
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
queryRcmdFeed('onLoad');
|
||||
}
|
||||
|
||||
// 返回顶部
|
||||
void animateToTop() {
|
||||
scrollController.animToTop();
|
||||
currentPage = 0;
|
||||
await queryData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -25,7 +26,6 @@ class RcmdPage extends StatefulWidget {
|
||||
class _RcmdPageState extends State<RcmdPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final RcmdController _rcmdController = Get.put(RcmdController());
|
||||
late Future _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -33,23 +33,21 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
|
||||
ScrollController scrollController = _rcmdController.scrollController;
|
||||
StreamController<bool> mainStream =
|
||||
Get.find<MainController>().bottomBarStream;
|
||||
StreamController<bool> searchBarStream =
|
||||
Get.find<HomeController>().searchBarStream;
|
||||
scrollController.addListener(
|
||||
_rcmdController.scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (_rcmdController.scrollController.position.pixels >=
|
||||
_rcmdController.scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle(
|
||||
'my-throttler', const Duration(milliseconds: 200), () {
|
||||
_rcmdController.onLoad();
|
||||
_rcmdController.onLoadMore();
|
||||
});
|
||||
}
|
||||
final ScrollDirection direction =
|
||||
scrollController.position.userScrollDirection;
|
||||
_rcmdController.scrollController.position.userScrollDirection;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
mainStream.add(true);
|
||||
searchBarStream.add(true);
|
||||
@@ -64,7 +62,6 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
@override
|
||||
void dispose() {
|
||||
_rcmdController.scrollController.removeListener(() {});
|
||||
_rcmdController.scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -81,44 +78,27 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _rcmdController.onRefresh();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _rcmdController.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(0, StyleString.cardSpace, 0, 0),
|
||||
sliver: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done &&
|
||||
snapshot.data != null) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => contentGrid(
|
||||
_rcmdController,
|
||||
_rcmdController.videoList.isEmpty
|
||||
? []
|
||||
: _rcmdController.videoList),
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data == null ? "" : data['msg'],
|
||||
padding: const EdgeInsets.only(top: StyleString.cardSpace),
|
||||
sliver: Obx(
|
||||
() => _rcmdController.loadingState.value is Loading ||
|
||||
_rcmdController.loadingState.value is Success
|
||||
? contentGrid(_rcmdController.loadingState.value)
|
||||
: HttpError(
|
||||
errMsg: _rcmdController.loadingState.value is Error
|
||||
? (_rcmdController.loadingState.value as Error)
|
||||
.errMsg
|
||||
: '没有相关数据',
|
||||
fn: () {
|
||||
setState(() {
|
||||
_futureBuilderFuture =
|
||||
_rcmdController.queryRcmdFeed('init');
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return contentGrid(_rcmdController, []);
|
||||
}
|
||||
},
|
||||
_rcmdController.loadingState.value =
|
||||
LoadingState.loading();
|
||||
_rcmdController.onRefresh();
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -141,7 +121,7 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget contentGrid(ctr, videoList) {
|
||||
Widget contentGrid(LoadingState loadingState) {
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
// 行间距
|
||||
@@ -155,12 +135,12 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return videoList!.isNotEmpty
|
||||
return loadingState is Success
|
||||
? VideoCardV(
|
||||
videoItem: videoList[index],
|
||||
videoItem: loadingState.response[index],
|
||||
longPress: () {
|
||||
_rcmdController.popupDialog
|
||||
.add(_createPopupDialog(videoList[index]));
|
||||
.add(_createPopupDialog(loadingState.response[index]));
|
||||
Overlay.of(context)
|
||||
.insert(_rcmdController.popupDialog.last!);
|
||||
},
|
||||
@@ -168,7 +148,7 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
)
|
||||
: const VideoCardVSkeleton();
|
||||
},
|
||||
childCount: videoList!.isNotEmpty ? videoList!.length : 10,
|
||||
childCount: loadingState is Success ? loadingState.response.length : 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,3 +13,31 @@ extension ScrollControllerExt on ScrollController {
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
extension ListExt<T> on List<T>? {
|
||||
bool get isNullOrEmpty => this == null || this!.isEmpty;
|
||||
|
||||
T? getOrNull(int index) {
|
||||
if (isNullOrEmpty) {
|
||||
return null;
|
||||
}
|
||||
return this![index];
|
||||
}
|
||||
|
||||
bool eq(List<T>? other) {
|
||||
if (this == null) {
|
||||
return other == null;
|
||||
}
|
||||
if (other == null || this!.length != other.length) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < this!.length; index += 1) {
|
||||
if (this![index] != other[index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ne(List<T>? other) => !eq(other);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user