feat: locate last viewed video

Closes #453

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-03-30 18:53:49 +08:00
parent e89bd2fedf
commit 40429021be
8 changed files with 228 additions and 128 deletions

View File

@@ -17,10 +17,12 @@ class VideoCardHMemberVideo extends StatelessWidget {
required this.videoItem, required this.videoItem,
this.onTap, this.onTap,
this.bvid, this.bvid,
this.fromViewAid,
}); });
final Item videoItem; final Item videoItem;
final VoidCallback? onTap; final VoidCallback? onTap;
final dynamic bvid; final dynamic bvid;
final String? fromViewAid;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -82,6 +84,21 @@ class VideoCardHMemberVideo extends StatelessWidget {
width: maxWidth, width: maxWidth,
height: maxHeight, height: maxHeight,
), ),
if (fromViewAid == videoItem.param)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black54,
),
child: Center(
child: const Text(
'上次观看',
style: TextStyle(fontSize: 15),
),
),
),
),
if (videoItem.badges?.isNotEmpty == true) if (videoItem.badges?.isNotEmpty == true)
PBadge( PBadge(
text: videoItem.badges! text: videoItem.badges!

View File

@@ -153,6 +153,7 @@ class MemberHttp {
int? next, int? next,
int? seasonId, int? seasonId,
int? seriesId, int? seriesId,
includeCursor,
}) async { }) async {
Map<String, String> data = { Map<String, String> data = {
if (aid != null) 'aid': aid.toString(), if (aid != null) 'aid': aid.toString(),
@@ -170,6 +171,7 @@ class MemberHttp {
'qn': type == ContributeType.video ? '80' : '32', 'qn': type == ContributeType.video ? '80' : '32',
if (order != null) 'order': order, if (order != null) 'order': order,
if (sort != null) 'sort': sort, if (sort != null) 'sort': sort,
if (includeCursor != null) 'include_cursor': includeCursor.toString(),
'statistics': Constants.statistics, 'statistics': Constants.statistics,
'vmid': mid.toString(), 'vmid': mid.toString(),
}; };
@@ -240,6 +242,7 @@ class MemberHttp {
static Future<LoadingState> space({ static Future<LoadingState> space({
int? mid, int? mid,
dynamic fromViewAid,
}) async { }) async {
Map<String, String> data = { Map<String, String> data = {
'build': '1462100', 'build': '1462100',
@@ -248,6 +251,7 @@ class MemberHttp {
'mobi_app': 'android_hd', 'mobi_app': 'android_hd',
'platform': 'android', 'platform': 'android',
's_locale': 'zh_CN', 's_locale': 'zh_CN',
if (fromViewAid != null) 'from_view_aid': fromViewAid,
'statistics': Constants.statistics, 'statistics': Constants.statistics,
'vmid': mid.toString(), 'vmid': mid.toString(),
}; };

View File

@@ -32,9 +32,9 @@ abstract class CommonController extends GetxController
@override @override
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
int currentPage = 1; late int currentPage = 1;
bool isLoading = false; bool isLoading = false;
bool isEnd = false; late bool isEnd = false;
Rx<LoadingState> loadingState = LoadingState.loading().obs; Rx<LoadingState> loadingState = LoadingState.loading().obs;
Future<LoadingState> customGetData(); Future<LoadingState> customGetData();

View File

@@ -62,129 +62,162 @@ class _MemberVideoState extends State<MemberVideo>
return switch (loadingState) { return switch (loadingState) {
Loading() => loadingWidget, Loading() => loadingWidget,
Success() => (loadingState.response as List?)?.isNotEmpty == true Success() => (loadingState.response as List?)?.isNotEmpty == true
? refreshIndicator( ? Stack(
onRefresh: () async { clipBehavior: Clip.none,
await _controller.onRefresh(); children: [
}, refreshIndicator(
child: CustomScrollView( onRefresh: () async {
slivers: [ await _controller.onRefresh();
SliverPersistentHeader( },
pinned: false, child: CustomScrollView(
floating: true, slivers: [
delegate: CustomSliverPersistentHeaderDelegate( SliverPersistentHeader(
extent: 40, pinned: false,
bgColor: Theme.of(context).colorScheme.surface, floating: true,
child: SizedBox( delegate: CustomSliverPersistentHeaderDelegate(
height: 40, extent: 40,
child: Row( bgColor: Theme.of(context).colorScheme.surface,
children: [ child: SizedBox(
const SizedBox(width: 8), height: 40,
Obx( child: Row(
() => Padding( children: [
padding: const EdgeInsets.only(left: 6), const SizedBox(width: 8),
child: Text( Obx(
_controller.count.value != -1 () => Padding(
? '${_controller.count.value}视频' padding: const EdgeInsets.only(left: 6),
: '', child: Text(
style: const TextStyle(fontSize: 13), _controller.count.value != -1
? '${_controller.count.value}视频'
: '',
style: const TextStyle(fontSize: 13),
),
),
), ),
), Obx(
), () => _controller.episodicButton.value.uri !=
Obx( null
() => _controller.episodicButton.value.uri != null ? Container(
? Container( height: 35,
height: 35, padding: EdgeInsets.only(
padding: EdgeInsets.only( left:
left: _controller.count.value != -1 _controller.count.value != -1
? 6 ? 6
: 0), : 0),
child: TextButton.icon( child: TextButton.icon(
onPressed: _controller.toViewPlayAll, onPressed:
icon: Icon( _controller.toViewPlayAll,
Icons.play_circle_outline_rounded, icon: Icon(
size: 16, Icons.play_circle_outline_rounded,
color: Theme.of(context) size: 16,
.colorScheme color: Theme.of(context)
.secondary, .colorScheme
), .secondary,
label: Text( ),
_controller label: Text(
.episodicButton.value.text ?? _controller.episodicButton.value
'播放全部', .text ??
style: TextStyle( '播放全部',
fontSize: 13, style: TextStyle(
color: Theme.of(context) fontSize: 13,
.colorScheme color: Theme.of(context)
.secondary, .colorScheme
.secondary,
),
),
), ),
), )
), : const SizedBox.shrink(),
)
: const SizedBox.shrink(),
),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,
color:
Theme.of(context).colorScheme.secondary,
), ),
label: Obx( const Spacer(),
() => Text( SizedBox(
widget.type == ContributeType.video height: 35,
? _controller.order.value == 'pubdate' child: TextButton.icon(
? '最新发布' onPressed: _controller.queryBySort,
: '最多播放' icon: Icon(
: _controller.sort.value == 'desc' Icons.sort,
? '默认' size: 16,
: '倒序',
style: TextStyle(
fontSize: 13,
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.secondary, .secondary,
), ),
label: Obx(
() => Text(
widget.type == ContributeType.video
? _controller.order.value ==
'pubdate'
? '最新发布'
: '最多播放'
: _controller.sort.value == 'desc'
? '默认'
: '倒序',
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.secondary,
),
),
),
), ),
), ),
), const SizedBox(width: 8),
],
), ),
const SizedBox(width: 8), ),
],
), ),
), ),
), SliverPadding(
), padding: EdgeInsets.only(
SliverPadding( top: StyleString.safeSpace - 5,
padding: EdgeInsets.only( bottom: MediaQuery.of(context).padding.bottom + 80,
top: StyleString.safeSpace - 5, ),
bottom: MediaQuery.of(context).padding.bottom + 80, sliver: SliverGrid(
), gridDelegate: SliverGridDelegateWithExtentAndRatio(
sliver: SliverGrid( mainAxisSpacing: 2,
gridDelegate: SliverGridDelegateWithExtentAndRatio( maxCrossAxisExtent: Grid.mediumCardWidth * 2,
mainAxisSpacing: 2, childAspectRatio: StyleString.aspectRatio * 2.2,
maxCrossAxisExtent: Grid.mediumCardWidth * 2, ),
childAspectRatio: StyleString.aspectRatio * 2.2, delegate: SliverChildBuilderDelegate(
(context, index) {
if (widget.type != ContributeType.season &&
index == loadingState.response.length - 1) {
_controller.onLoadMore();
}
return VideoCardHMemberVideo(
videoItem: loadingState.response[index],
fromViewAid: _controller.fromViewAid,
);
},
childCount: loadingState.response.length,
),
),
), ),
delegate: SliverChildBuilderDelegate( ],
(context, index) { ),
if (widget.type != ContributeType.season && ),
index == loadingState.response.length - 1) { if (widget.type == ContributeType.video &&
_controller.onLoadMore(); _controller.fromViewAid?.isNotEmpty == true &&
} _controller.isLocating != true)
return VideoCardHMemberVideo( Positioned(
videoItem: loadingState.response[index], right: 15,
); bottom: 15,
child: SafeArea(
top: false,
left: false,
child: FloatingActionButton.extended(
onPressed: () {
_controller
..isLocating = true
..lastAid = _controller.fromViewAid
..currentPage = 0
..loadingState.value = LoadingState.loading()
..queryData();
}, },
childCount: loadingState.response.length, label: const Text('定位至上次观看'),
), ),
), ),
), ),
], ],
),
) )
: scrollErrorWidget( : scrollErrorWidget(
callback: _controller.onReload, callback: _controller.onReload,

View File

@@ -27,7 +27,6 @@ class MemberVideoCtr extends CommonController {
int? seasonId; int? seasonId;
int? seriesId; int? seriesId;
final int mid; final int mid;
String? aid;
late RxString order = 'pubdate'.obs; late RxString order = 'pubdate'.obs;
late RxString sort = 'desc'.obs; late RxString sort = 'desc'.obs;
RxInt count = (-1).obs; RxInt count = (-1).obs;
@@ -36,18 +35,36 @@ class MemberVideoCtr extends CommonController {
final String? username; final String? username;
final String? title; final String? title;
String? firstAid;
String? lastAid;
String? fromViewAid;
bool? isLocating;
bool? isLoadPrevious;
bool? hasPrev;
@override @override
Future onRefresh() async { Future onRefresh() async {
aid = null; if (isLocating == true) {
next = null; if (hasPrev == true) {
currentPage = 0; isLoadPrevious = true;
isEnd = false; await queryData();
await queryData(); }
} else {
firstAid = null;
lastAid = null;
next = null;
isEnd = false;
currentPage = 0;
await queryData();
}
} }
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (type == ContributeType.video) {
fromViewAid = Get.parameters['from_view_aid'];
}
currentPage = 0; currentPage = 0;
queryData(); queryData();
} }
@@ -58,20 +75,31 @@ class MemberVideoCtr extends CommonController {
episodicButton.value = data.episodicButton ?? EpisodicButton(); episodicButton.value = data.episodicButton ?? EpisodicButton();
episodicButton.refresh(); episodicButton.refresh();
next = data.next; next = data.next;
aid = data.item?.lastOrNull?.param; if (currentPage == 0 || isLoadPrevious == true) {
if ((type == ContributeType.video hasPrev = data.hasPrev;
? data.hasNext == false }
: data.next == 0) || if (currentPage == 0 || isLoadPrevious != true) {
data.item.isNullOrEmpty) { if ((type == ContributeType.video
isEnd = true; ? data.hasNext == false
: data.next == 0) ||
data.item.isNullOrEmpty) {
isEnd = true;
}
} }
count.value = type == ContributeType.season count.value = type == ContributeType.season
? (data.item?.length ?? -1) ? (data.item?.length ?? -1)
: (data.count ?? -1); : (data.count ?? -1);
if (currentPage != 0 && loadingState.value is Success) { if (currentPage != 0 && loadingState.value is Success) {
data.item ??= <Item>[]; data.item ??= <Item>[];
data.item!.insertAll(0, (loadingState.value as Success).response); if (isLoadPrevious == true) {
data.item!.addAll((loadingState.value as Success).response);
} else {
data.item!.insertAll(0, (loadingState.value as Success).response);
}
} }
firstAid = data.item?.firstOrNull?.param;
lastAid = data.item?.lastOrNull?.param;
isLoadPrevious = null;
loadingState.value = LoadingState.success(data.item); loadingState.value = LoadingState.success(data.item);
return true; return true;
} }
@@ -80,17 +108,27 @@ class MemberVideoCtr extends CommonController {
Future<LoadingState> customGetData() => MemberHttp.spaceArchive( Future<LoadingState> customGetData() => MemberHttp.spaceArchive(
type: type, type: type,
mid: mid, mid: mid,
aid: type == ContributeType.video ? aid : null, aid: type == ContributeType.video
? isLoadPrevious == true
? firstAid
: lastAid
: null,
order: type == ContributeType.video ? order.value : null, order: type == ContributeType.video ? order.value : null,
sort: type == ContributeType.video ? null : sort.value, sort: type == ContributeType.video
? isLoadPrevious == true
? 'asc'
: null
: sort.value,
pn: type == ContributeType.charging ? currentPage : null, pn: type == ContributeType.charging ? currentPage : null,
next: next, next: next,
seasonId: seasonId, seasonId: seasonId,
seriesId: seriesId, seriesId: seriesId,
includeCursor: isLocating == true && currentPage == 0 ? true : null,
); );
queryBySort() { queryBySort() {
if (type == ContributeType.video) { if (type == ContributeType.video) {
isLocating = null;
order.value = order.value == 'pubdate' ? 'click' : 'pubdate'; order.value = order.value == 'pubdate' ? 'click' : 'pubdate';
} else { } else {
sort.value = sort.value == 'desc' ? 'asc' : 'desc'; sort.value = sort.value == 'desc' ? 'asc' : 'desc';

View File

@@ -35,6 +35,7 @@ class MemberControllerNew extends CommonController
RxInt contributeInitialIndex = 0.obs; RxInt contributeInitialIndex = 0.obs;
double? top; double? top;
bool? hasSeasonOrSeries; bool? hasSeasonOrSeries;
final fromViewAid = Get.parameters['from_view_aid'];
@override @override
void onInit() { void onInit() {
@@ -95,7 +96,7 @@ class MemberControllerNew extends CommonController
} }
if (initialIndex == -1) { if (initialIndex == -1) {
if (data.defaultTab == 'video') { if (data.defaultTab == 'video') {
data.defaultTab = 'dynamic'; data.defaultTab = 'contribute';
} }
initialIndex = tab2!.indexWhere((item) { initialIndex = tab2!.indexWhere((item) {
return item.param == data.defaultTab; return item.param == data.defaultTab;
@@ -137,7 +138,10 @@ class MemberControllerNew extends CommonController
} }
@override @override
Future<LoadingState> customGetData() => MemberHttp.space(mid: mid); Future<LoadingState> customGetData() => MemberHttp.space(
mid: mid,
fromViewAid: fromViewAid,
);
Future blockUser(BuildContext context) async { Future blockUser(BuildContext context) async {
if (ownerMid == null) { if (ownerMid == null) {

View File

@@ -258,7 +258,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// ? videoDetail.owner!.face // ? videoDetail.owner!.face
// : videoItem['owner'].face; // : videoItem['owner'].face;
Get.toNamed( Get.toNamed(
'/member?mid=$mid', '/member?mid=$mid&from_view_aid=${videoDetailCtr.oid.value}',
// arguments: { // arguments: {
// 'face': face, // 'face': face,
// 'heroTag': memberHeroTag, // 'heroTag': memberHeroTag,

View File

@@ -50,8 +50,12 @@ class HorizontalMemberPageController extends CommonController {
@override @override
bool customHandleResponse(Success response) { bool customHandleResponse(Success response) {
final data = response.response; final data = response.response;
hasPrev = data['page']['has_prev']; if (currentPage == 0 || isLoadPrevious == true) {
hasNext = data['page']['has_next']; hasPrev = data['page']['has_prev'];
}
if (currentPage == 0 || isLoadPrevious != true) {
hasNext = data['page']['has_next'];
}
if (currentPage != 0 && loadingState.value is Success) { if (currentPage != 0 && loadingState.value is Success) {
data['items'] ??= []; data['items'] ??= [];