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),
|
label: Utils.videoItemSemantics(videoItem),
|
||||||
excludeSemantics: true,
|
excludeSemantics: true,
|
||||||
child: Card(
|
child: Card(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: GestureDetector(
|
child: InkWell(
|
||||||
onLongPress: () {
|
onTap: () async => onPushDetail(heroTag),
|
||||||
if (longPress != null) {
|
onLongPress: () {
|
||||||
longPress!();
|
if (longPress != null) {
|
||||||
}
|
longPress!();
|
||||||
},
|
}
|
||||||
// onLongPressEnd: (details) {
|
},
|
||||||
// if (longPressEnd != null) {
|
child: Column(
|
||||||
// longPressEnd!();
|
children: [
|
||||||
// }
|
AspectRatio(
|
||||||
// },
|
aspectRatio: StyleString.aspectRatio,
|
||||||
child: InkWell(
|
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||||
onTap: () async => onPushDetail(heroTag),
|
double maxWidth = boxConstraints.maxWidth;
|
||||||
child: Column(
|
double maxHeight = boxConstraints.maxHeight;
|
||||||
children: [
|
return Stack(
|
||||||
AspectRatio(
|
children: [
|
||||||
aspectRatio: StyleString.aspectRatio,
|
Hero(
|
||||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
tag: heroTag,
|
||||||
double maxWidth = boxConstraints.maxWidth;
|
child: NetworkImgLayer(
|
||||||
double maxHeight = boxConstraints.maxHeight;
|
src: videoItem.pic,
|
||||||
return Stack(
|
width: maxWidth,
|
||||||
children: [
|
height: maxHeight,
|
||||||
Hero(
|
),
|
||||||
tag: heroTag,
|
),
|
||||||
child: NetworkImgLayer(
|
if (videoItem.duration > 0)
|
||||||
src: videoItem.pic,
|
PBadge(
|
||||||
width: maxWidth,
|
bottom: 6,
|
||||||
height: maxHeight,
|
right: 7,
|
||||||
),
|
size: 'small',
|
||||||
),
|
type: 'gray',
|
||||||
if (videoItem.duration > 0)
|
text: Utils.timeFormat(videoItem.duration),
|
||||||
PBadge(
|
// semanticsLabel:
|
||||||
bottom: 6,
|
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
|
||||||
right: 7,
|
)
|
||||||
size: 'small',
|
],
|
||||||
type: 'gray',
|
);
|
||||||
text: Utils.timeFormat(videoItem.duration),
|
}),
|
||||||
// semanticsLabel:
|
|
||||||
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
VideoContent(videoItem: videoItem)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
VideoContent(videoItem: videoItem)
|
||||||
)),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (videoItem.goto == 'av')
|
if (videoItem.goto == 'av')
|
||||||
Positioned(
|
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 'dart:developer';
|
||||||
|
import 'package:PiliPalaX/http/loading_state.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import '../common/constants.dart';
|
import '../common/constants.dart';
|
||||||
@@ -34,7 +34,8 @@ class VideoHttp {
|
|||||||
static Box userInfoCache = GStorage.userInfo;
|
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(
|
var res = await Request().get(
|
||||||
Api.recommendListWeb,
|
Api.recommendListWeb,
|
||||||
data: {
|
data: {
|
||||||
@@ -49,10 +50,8 @@ class VideoHttp {
|
|||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
List<RecVideoItemModel> list = [];
|
List<RecVideoItemModel> list = [];
|
||||||
List<int> blackMidsList = localCache
|
List<int> blackMidsList =
|
||||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||||
.map<int>((e) => e as int)
|
|
||||||
.toList();
|
|
||||||
for (var i in res.data['data']['item']) {
|
for (var i in res.data['data']['item']) {
|
||||||
//过滤掉live与ad,以及拉黑用户
|
//过滤掉live与ad,以及拉黑用户
|
||||||
if (i['goto'] == 'av' &&
|
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 {
|
} else {
|
||||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
return LoadingState.error(res.data['message']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加额外的loginState变量模拟未登录状态
|
// 添加额外的loginState变量模拟未登录状态
|
||||||
static Future rcmdVideoListApp(
|
static Future<LoadingState> rcmdVideoListApp(
|
||||||
{bool loginStatus = true, required int freshIdx}) async {
|
{bool loginStatus = true, required int freshIdx}) async {
|
||||||
var data = {
|
var data = {
|
||||||
'access_key': loginStatus
|
'access_key': loginStatus
|
||||||
@@ -138,10 +141,8 @@ class VideoHttp {
|
|||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
List<RecVideoItemAppModel> list = [];
|
List<RecVideoItemAppModel> list = [];
|
||||||
List<int> blackMidsList = localCache
|
List<int> blackMidsList =
|
||||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
localCache.get(LocalCacheKey.blackMidsList, defaultValue: <int>[]);
|
||||||
.map<int>((e) => e as int)
|
|
||||||
.toList();
|
|
||||||
for (var i in res.data['data']['items']) {
|
for (var i in res.data['data']['items']) {
|
||||||
// 屏蔽推广和拉黑用户
|
// 屏蔽推广和拉黑用户
|
||||||
if (i['card_goto'] != 'ad_av' &&
|
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 {
|
} 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 {
|
static Future<LoadingState> hotVideoList(
|
||||||
try {
|
{required int pn, required int ps}) async {
|
||||||
var res = await Request().get(
|
var res = await Request().get(
|
||||||
Api.hotList,
|
Api.hotList,
|
||||||
data: {'pn': pn, 'ps': ps},
|
data: {'pn': pn, 'ps': ps},
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
List<HotVideoItemModel> list = [];
|
List<HotVideoItemModel> list = [];
|
||||||
List<int> blackMidsList = localCache
|
List<int> blackMidsList = localCache
|
||||||
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
.get(LocalCacheKey.blackMidsList, defaultValue: [-1])
|
||||||
.map<int>((e) => e as int)
|
.map<int>((e) => e as int)
|
||||||
.toList();
|
.toList();
|
||||||
for (var i in res.data['data']['list']) {
|
for (var i in res.data['data']['list']) {
|
||||||
if (!blackMidsList.contains(i['owner']['mid'])) {
|
if (!blackMidsList.contains(i['owner']['mid'])) {
|
||||||
list.add(HotVideoItemModel.fromJson(i));
|
list.add(HotVideoItemModel.fromJson(i));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {'status': true, 'data': list};
|
|
||||||
} else {
|
|
||||||
return {'status': false, 'data': [], 'msg': res.data['message']};
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
if (list.isNotEmpty) {
|
||||||
return {'status': false, 'data': [], 'msg': err};
|
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:PiliPalaX/http/loading_state.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:PiliPalaX/pages/common/common_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:PiliPalaX/http/video.dart';
|
import 'package:PiliPalaX/http/video.dart';
|
||||||
import 'package:PiliPalaX/models/model_hot_video_item.dart';
|
|
||||||
|
|
||||||
class HotController extends GetxController {
|
class HotController extends CommonController {
|
||||||
final ScrollController scrollController = ScrollController();
|
|
||||||
final int _count = 20;
|
final int _count = 20;
|
||||||
int _currentPage = 1;
|
|
||||||
RxList<HotVideoItemModel> videoList = <HotVideoItemModel>[].obs;
|
|
||||||
bool isLoadingMore = false;
|
|
||||||
bool flag = false;
|
|
||||||
List<OverlayEntry?> popupDialog = <OverlayEntry?>[];
|
List<OverlayEntry?> popupDialog = <OverlayEntry?>[];
|
||||||
|
|
||||||
// 获取推荐
|
@override
|
||||||
Future queryHotFeed(type) async {
|
void onInit() {
|
||||||
if (type != 'onLoad') {
|
super.onInit();
|
||||||
_currentPage = 1;
|
queryData();
|
||||||
}
|
|
||||||
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
|
||||||
Future onRefresh() async {
|
Future<LoadingState> customGetData() => VideoHttp.hotVideoList(
|
||||||
await queryHotFeed('onRefresh');
|
pn: currentPage,
|
||||||
}
|
ps: _count,
|
||||||
|
);
|
||||||
// 上拉加载
|
|
||||||
Future onLoad() async {
|
|
||||||
await queryHotFeed('onLoad');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回顶部
|
|
||||||
void animateToTop() {
|
|
||||||
scrollController.animToTop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:PiliPalaX/http/loading_state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -23,8 +24,6 @@ class HotPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||||
final HotController _hotController = Get.put(HotController());
|
final HotController _hotController = Get.put(HotController());
|
||||||
List videoList = [];
|
|
||||||
Future? _futureBuilderFuture;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
@@ -32,7 +31,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_futureBuilderFuture = _hotController.queryHotFeed('init');
|
|
||||||
StreamController<bool> mainStream =
|
StreamController<bool> mainStream =
|
||||||
Get.find<MainController>().bottomBarStream;
|
Get.find<MainController>().bottomBarStream;
|
||||||
StreamController<bool> searchBarStream =
|
StreamController<bool> searchBarStream =
|
||||||
@@ -41,10 +39,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
|||||||
() {
|
() {
|
||||||
if (_hotController.scrollController.position.pixels >=
|
if (_hotController.scrollController.position.pixels >=
|
||||||
_hotController.scrollController.position.maxScrollExtent - 200) {
|
_hotController.scrollController.position.maxScrollExtent - 200) {
|
||||||
if (!_hotController.isLoadingMore) {
|
_hotController.onLoadMore();
|
||||||
_hotController.isLoadingMore = true;
|
|
||||||
_hotController.onLoad();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final ScrollDirection direction =
|
final ScrollDirection direction =
|
||||||
@@ -63,7 +58,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_hotController.scrollController.removeListener(() {});
|
_hotController.scrollController.removeListener(() {});
|
||||||
_hotController.scrollController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,69 +74,29 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
|||||||
slivers: [
|
slivers: [
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
// 单列布局 EdgeInsets.zero
|
// 单列布局 EdgeInsets.zero
|
||||||
padding: const EdgeInsets.fromLTRB(StyleString.safeSpace,
|
padding: EdgeInsets.fromLTRB(
|
||||||
StyleString.safeSpace - 5, StyleString.safeSpace, 0),
|
StyleString.safeSpace,
|
||||||
sliver: FutureBuilder(
|
StyleString.safeSpace - 5,
|
||||||
future: _futureBuilderFuture,
|
StyleString.safeSpace,
|
||||||
builder: (context, snapshot) {
|
MediaQuery.of(context).padding.bottom + 10,
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
),
|
||||||
Map data = snapshot.data as Map;
|
sliver: Obx(
|
||||||
if (data['status']) {
|
() => _hotController.loadingState.value is Loading
|
||||||
return Obx(
|
? _buildSkeleton()
|
||||||
() => SliverGrid(
|
: _hotController.loadingState.value is Success
|
||||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
? _buildBody(_hotController.loadingState.value as Success)
|
||||||
mainAxisSpacing: StyleString.safeSpace,
|
: HttpError(
|
||||||
crossAxisSpacing: StyleString.safeSpace,
|
errMsg: _hotController.loadingState.value is Error
|
||||||
maxCrossAxisExtent: Grid.maxRowWidth * 2,
|
? (_hotController.loadingState.value as Error)
|
||||||
childAspectRatio: StyleString.aspectRatio * 2.4,
|
.errMsg
|
||||||
mainAxisExtent: 0),
|
: '没有相关数据',
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
fn: () {
|
||||||
return VideoCardH(
|
_hotController.loadingState.value =
|
||||||
videoItem: _hotController.videoList[index],
|
LoadingState.loading();
|
||||||
showPubdate: true,
|
_hotController.onRefresh();
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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:PiliPalaX/http/loading_state.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/models/home/rcmd/result.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';
|
import 'package:PiliPalaX/utils/storage.dart';
|
||||||
|
|
||||||
class RcmdController extends GetxController {
|
class RcmdController extends CommonController {
|
||||||
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;
|
|
||||||
late bool enableSaveLastData;
|
late bool enableSaveLastData;
|
||||||
late String defaultRcmdType = 'web';
|
late String defaultRcmdType = 'web';
|
||||||
late RxList<dynamic> videoList;
|
List<OverlayEntry?> popupDialog = <OverlayEntry?>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
enableSaveLastData =
|
enableSaveLastData = GStorage.setting
|
||||||
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
|
.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
|
||||||
defaultRcmdType =
|
defaultRcmdType = GStorage.setting
|
||||||
setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
|
.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web');
|
||||||
if (defaultRcmdType == 'web') {
|
|
||||||
videoList = <RecVideoItemModel>[].obs;
|
currentPage = 0;
|
||||||
} else {
|
queryData();
|
||||||
videoList = <RecVideoItemAppModel>[].obs;
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取推荐
|
@override
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉刷新
|
|
||||||
Future onRefresh() async {
|
Future onRefresh() async {
|
||||||
queryRcmdFeed('onRefresh');
|
currentPage = 0;
|
||||||
}
|
await queryData();
|
||||||
|
|
||||||
// 上拉加载
|
|
||||||
Future onLoad() async {
|
|
||||||
queryRcmdFeed('onLoad');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回顶部
|
|
||||||
void animateToTop() {
|
|
||||||
scrollController.animToTop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:PiliPalaX/http/loading_state.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@@ -25,7 +26,6 @@ class RcmdPage extends StatefulWidget {
|
|||||||
class _RcmdPageState extends State<RcmdPage>
|
class _RcmdPageState extends State<RcmdPage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
final RcmdController _rcmdController = Get.put(RcmdController());
|
final RcmdController _rcmdController = Get.put(RcmdController());
|
||||||
late Future _futureBuilderFuture;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
@@ -33,23 +33,21 @@ class _RcmdPageState extends State<RcmdPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
|
|
||||||
ScrollController scrollController = _rcmdController.scrollController;
|
|
||||||
StreamController<bool> mainStream =
|
StreamController<bool> mainStream =
|
||||||
Get.find<MainController>().bottomBarStream;
|
Get.find<MainController>().bottomBarStream;
|
||||||
StreamController<bool> searchBarStream =
|
StreamController<bool> searchBarStream =
|
||||||
Get.find<HomeController>().searchBarStream;
|
Get.find<HomeController>().searchBarStream;
|
||||||
scrollController.addListener(
|
_rcmdController.scrollController.addListener(
|
||||||
() {
|
() {
|
||||||
if (scrollController.position.pixels >=
|
if (_rcmdController.scrollController.position.pixels >=
|
||||||
scrollController.position.maxScrollExtent - 200) {
|
_rcmdController.scrollController.position.maxScrollExtent - 200) {
|
||||||
EasyThrottle.throttle(
|
EasyThrottle.throttle(
|
||||||
'my-throttler', const Duration(milliseconds: 200), () {
|
'my-throttler', const Duration(milliseconds: 200), () {
|
||||||
_rcmdController.onLoad();
|
_rcmdController.onLoadMore();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
final ScrollDirection direction =
|
final ScrollDirection direction =
|
||||||
scrollController.position.userScrollDirection;
|
_rcmdController.scrollController.position.userScrollDirection;
|
||||||
if (direction == ScrollDirection.forward) {
|
if (direction == ScrollDirection.forward) {
|
||||||
mainStream.add(true);
|
mainStream.add(true);
|
||||||
searchBarStream.add(true);
|
searchBarStream.add(true);
|
||||||
@@ -64,7 +62,6 @@ class _RcmdPageState extends State<RcmdPage>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_rcmdController.scrollController.removeListener(() {});
|
_rcmdController.scrollController.removeListener(() {});
|
||||||
_rcmdController.scrollController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,44 +78,27 @@ class _RcmdPageState extends State<RcmdPage>
|
|||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await _rcmdController.onRefresh();
|
await _rcmdController.onRefresh();
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
|
||||||
},
|
},
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _rcmdController.scrollController,
|
controller: _rcmdController.scrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding:
|
padding: const EdgeInsets.only(top: StyleString.cardSpace),
|
||||||
const EdgeInsets.fromLTRB(0, StyleString.cardSpace, 0, 0),
|
sliver: Obx(
|
||||||
sliver: FutureBuilder(
|
() => _rcmdController.loadingState.value is Loading ||
|
||||||
future: _futureBuilderFuture,
|
_rcmdController.loadingState.value is Success
|
||||||
builder: (context, snapshot) {
|
? contentGrid(_rcmdController.loadingState.value)
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
: HttpError(
|
||||||
snapshot.data != null) {
|
errMsg: _rcmdController.loadingState.value is Error
|
||||||
Map data = snapshot.data as Map;
|
? (_rcmdController.loadingState.value as Error)
|
||||||
if (data['status']) {
|
.errMsg
|
||||||
return Obx(
|
: '没有相关数据',
|
||||||
() => contentGrid(
|
|
||||||
_rcmdController,
|
|
||||||
_rcmdController.videoList.isEmpty
|
|
||||||
? []
|
|
||||||
: _rcmdController.videoList),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return HttpError(
|
|
||||||
errMsg: data == null ? "" : data['msg'],
|
|
||||||
fn: () {
|
fn: () {
|
||||||
setState(() {
|
_rcmdController.loadingState.value =
|
||||||
_futureBuilderFuture =
|
LoadingState.loading();
|
||||||
_rcmdController.queryRcmdFeed('init');
|
_rcmdController.onRefresh();
|
||||||
});
|
}),
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return contentGrid(_rcmdController, []);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -141,7 +121,7 @@ class _RcmdPageState extends State<RcmdPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget contentGrid(ctr, videoList) {
|
Widget contentGrid(LoadingState loadingState) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||||
// 行间距
|
// 行间距
|
||||||
@@ -155,12 +135,12 @@ class _RcmdPageState extends State<RcmdPage>
|
|||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return videoList!.isNotEmpty
|
return loadingState is Success
|
||||||
? VideoCardV(
|
? VideoCardV(
|
||||||
videoItem: videoList[index],
|
videoItem: loadingState.response[index],
|
||||||
longPress: () {
|
longPress: () {
|
||||||
_rcmdController.popupDialog
|
_rcmdController.popupDialog
|
||||||
.add(_createPopupDialog(videoList[index]));
|
.add(_createPopupDialog(loadingState.response[index]));
|
||||||
Overlay.of(context)
|
Overlay.of(context)
|
||||||
.insert(_rcmdController.popupDialog.last!);
|
.insert(_rcmdController.popupDialog.last!);
|
||||||
},
|
},
|
||||||
@@ -168,7 +148,7 @@ class _RcmdPageState extends State<RcmdPage>
|
|||||||
)
|
)
|
||||||
: const VideoCardVSkeleton();
|
: 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);
|
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