mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-11 21:31:40 +08:00
feat: space opus
Closes #833 Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -26,6 +26,12 @@ class Constants {
|
||||
'{"appId":5,"platform":3,"version":"1.46.2","abtest":""}';
|
||||
// 请求时会自动encodeComponent
|
||||
|
||||
// app
|
||||
static const String userAgentApp =
|
||||
'Mozilla/5.0 BiliDroid/8.43.0 (bbcallen@gmail.com) os/android model/android mobi_app/android build/8430300 channel/bili innerVer/8430300 osVer/15 network/2';
|
||||
static const String statisticsApp =
|
||||
'{"appId":5,"platform":3,"version":"8.43.0","abtest":""}';
|
||||
|
||||
static const urlPattern =
|
||||
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]';
|
||||
|
||||
|
||||
48
lib/common/skeleton/space_opus.dart
Normal file
48
lib/common/skeleton/space_opus.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SpaceOpusSkeleton extends StatelessWidget {
|
||||
const SpaceOpusSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final surface = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: (0.68 + 0.82 * Utils.random.nextDouble()) *
|
||||
constraints.maxWidth,
|
||||
color: surface,
|
||||
),
|
||||
Container(
|
||||
height: 10,
|
||||
color: surface,
|
||||
margin: const EdgeInsets.all(10),
|
||||
width: constraints.maxWidth * 0.7,
|
||||
),
|
||||
Container(
|
||||
height: 10,
|
||||
color: surface,
|
||||
margin:
|
||||
const EdgeInsets.only(left: 10, right: 10, bottom: 10),
|
||||
width: constraints.maxWidth,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
|
||||
@@ -15,7 +15,7 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/app/dynamic/v1.pb.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/app/dynamic/v2.pb.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/im/interfaces/v1.pb.dart';
|
||||
@@ -32,8 +33,12 @@ class GrpcUrl {
|
||||
// static const popular = '/bilibili.app.show.v1.Popular/Index';
|
||||
|
||||
// dynamic
|
||||
static const dynRed = '/bilibili.app.dynamic.v1.Dynamic/DynRed';
|
||||
// static const dynSpace = '/bilibili.app.dynamic.v2.Dynamic/DynSpace';
|
||||
static const dynV1 = '/bilibili.app.dynamic.v1.Dynamic';
|
||||
static const dynV2 = '/bilibili.app.dynamic.v2.Dynamic';
|
||||
static const opusV2 = '/bilibili.app.dynamic.v2.Opus';
|
||||
static const dynRed = '$dynV1/DynRed';
|
||||
static const opusSpaceFlow = '$opusV2/OpusSpaceFlow';
|
||||
// static const dynSpace = '$dynV2/DynSpace';
|
||||
|
||||
// danmaku
|
||||
static const dmSegMobile = '/bilibili.community.service.dm.v1.DM/DmSegMobile';
|
||||
@@ -397,4 +402,23 @@ class GrpcRepo {
|
||||
ClearUnreadReply.fromBuffer,
|
||||
);
|
||||
}
|
||||
|
||||
static Future opusSpaceFlow({
|
||||
required int hostMid,
|
||||
String? next,
|
||||
required String filterType,
|
||||
}) {
|
||||
return _request(
|
||||
GrpcUrl.opusSpaceFlow,
|
||||
OpusSpaceFlowReq(
|
||||
hostMid: Int64(hostMid),
|
||||
pagination: Pagination(
|
||||
pageSize: 20,
|
||||
next: next,
|
||||
),
|
||||
filterType: filterType,
|
||||
),
|
||||
OpusSpaceFlowResp.fromBuffer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,4 +827,6 @@ class Api {
|
||||
'${HttpString.appBaseUrl}/x/topic/web/details/top';
|
||||
|
||||
static const String topicFeed = '/x/polymer/web-dynamic/v1/feed/topic';
|
||||
|
||||
static const String spaceOpus = '/x/polymer/web-dynamic/v1/opus/feed/space';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/app/dynamic/v2.pb.dart'
|
||||
show OpusSpaceFlowResp;
|
||||
import 'package:PiliPlus/grpc/grpc_repo.dart';
|
||||
import 'package:PiliPlus/http/api.dart';
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
@@ -18,6 +21,7 @@ import 'package:PiliPlus/models/space/data.dart';
|
||||
import 'package:PiliPlus/models/space_archive/data.dart';
|
||||
import 'package:PiliPlus/models/space_article/data.dart';
|
||||
import 'package:PiliPlus/models/space_fav/space_fav.dart';
|
||||
import 'package:PiliPlus/models/space_opus/data.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/wbi_sign.dart';
|
||||
@@ -65,15 +69,15 @@ class MemberHttp {
|
||||
required int page,
|
||||
}) async {
|
||||
Map<String, String> data = {
|
||||
'build': '1462100',
|
||||
'build': '8430300',
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
'mobi_app': 'android_hd',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'pn': '$page',
|
||||
'pn': page.toString(),
|
||||
'ps': '10',
|
||||
's_locale': 'zh_CN',
|
||||
'statistics': Constants.statistics,
|
||||
'statistics': Constants.statisticsApp,
|
||||
'vmid': mid.toString(),
|
||||
};
|
||||
dynamic res = await Request().get(
|
||||
@@ -82,7 +86,7 @@ class MemberHttp {
|
||||
options: Options(
|
||||
headers: {
|
||||
'bili-http-engine': 'cronet',
|
||||
'user-agent': Constants.userAgent,
|
||||
'user-agent': Constants.userAgentApp,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -97,13 +101,13 @@ class MemberHttp {
|
||||
required int mid,
|
||||
}) async {
|
||||
Map<String, String> data = {
|
||||
'build': '1462100',
|
||||
'build': '8430300',
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
'mobi_app': 'android_hd',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
's_locale': 'zh_CN',
|
||||
'statistics': Constants.statistics,
|
||||
'statistics': Constants.statisticsApp,
|
||||
'up_mid': mid.toString(),
|
||||
};
|
||||
dynamic res = await Request().get(
|
||||
@@ -112,7 +116,7 @@ class MemberHttp {
|
||||
options: Options(
|
||||
headers: {
|
||||
'bili-http-engine': 'cronet',
|
||||
'user-agent': Constants.userAgent,
|
||||
'user-agent': Constants.userAgentApp,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -156,10 +160,10 @@ class MemberHttp {
|
||||
}) async {
|
||||
Map<String, String> data = {
|
||||
if (aid != null) 'aid': aid.toString(),
|
||||
'build': '1462100',
|
||||
'build': '8430300',
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
'mobi_app': 'android_hd',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
's_locale': 'zh_CN',
|
||||
'ps': '20',
|
||||
@@ -171,7 +175,7 @@ class MemberHttp {
|
||||
if (order != null) 'order': order,
|
||||
if (sort != null) 'sort': sort,
|
||||
if (includeCursor != null) 'include_cursor': includeCursor.toString(),
|
||||
'statistics': Constants.statistics,
|
||||
'statistics': Constants.statisticsApp,
|
||||
'vmid': mid.toString(),
|
||||
};
|
||||
dynamic res = await Request().get(
|
||||
@@ -186,7 +190,7 @@ class MemberHttp {
|
||||
options: Options(
|
||||
headers: {
|
||||
'bili-http-engine': 'cronet',
|
||||
'user-agent': Constants.userAgent,
|
||||
'user-agent': Constants.userAgentApp,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -213,13 +217,13 @@ class MemberHttp {
|
||||
'cid': cid.toString(),
|
||||
'contain': contain.toString(),
|
||||
'index': index.toString(),
|
||||
'build': '1462100',
|
||||
'build': '8430300',
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
'mobi_app': 'android_hd',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
's_locale': 'zh_CN',
|
||||
'statistics': Constants.statistics,
|
||||
'statistics': Constants.statisticsApp,
|
||||
'vmid': mid.toString(),
|
||||
};
|
||||
dynamic res = await Request().get(
|
||||
@@ -228,7 +232,7 @@ class MemberHttp {
|
||||
options: Options(
|
||||
headers: {
|
||||
'bili-http-engine': 'cronet',
|
||||
'user-agent': Constants.userAgent,
|
||||
'user-agent': Constants.userAgentApp,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -244,14 +248,14 @@ class MemberHttp {
|
||||
dynamic fromViewAid,
|
||||
}) async {
|
||||
Map<String, String> data = {
|
||||
'build': '1462100',
|
||||
'build': '8430300',
|
||||
'c_locale': 'zh_CN',
|
||||
'channel': 'yingyongbao',
|
||||
'mobi_app': 'android_hd',
|
||||
'channel': 'bili',
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
's_locale': 'zh_CN',
|
||||
if (fromViewAid != null) 'from_view_aid': fromViewAid,
|
||||
'statistics': Constants.statistics,
|
||||
'statistics': Constants.statisticsApp,
|
||||
'vmid': mid.toString(),
|
||||
};
|
||||
dynamic res = await Request().get(
|
||||
@@ -260,7 +264,7 @@ class MemberHttp {
|
||||
options: Options(
|
||||
headers: {
|
||||
'bili-http-engine': 'cronet',
|
||||
'user-agent': Constants.userAgent,
|
||||
'user-agent': Constants.userAgentApp,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -843,4 +847,44 @@ class MemberHttp {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<OpusSpaceFlowResp>> opusSpaceFlow({
|
||||
required int hostMid,
|
||||
String? next,
|
||||
required String filterType,
|
||||
}) async {
|
||||
var res = await GrpcRepo.opusSpaceFlow(
|
||||
hostMid: hostMid,
|
||||
next: next,
|
||||
filterType: filterType,
|
||||
);
|
||||
if (res['status']) {
|
||||
return LoadingState.success(res['data']);
|
||||
} else {
|
||||
return LoadingState.error(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<SpaceOpusData>> spaceOpus({
|
||||
required int hostMid,
|
||||
required int page,
|
||||
String offset = '',
|
||||
String type = 'all',
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
Api.spaceOpus,
|
||||
queryParameters: await WbiSign.makSign({
|
||||
'host_mid': hostMid,
|
||||
'page': page,
|
||||
'offset': offset,
|
||||
'type': type,
|
||||
'web_location': 333.1387,
|
||||
}),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return LoadingState.success(SpaceOpusData.fromJson(res.data['data']));
|
||||
} else {
|
||||
return LoadingState.error(res.data['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
lib/models/space/filter.dart
Normal file
31
lib/models/space/filter.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
class SpaceTabFilter {
|
||||
SpaceTabFilter({
|
||||
this.text,
|
||||
required this.meta,
|
||||
this.tabName,
|
||||
});
|
||||
|
||||
String? text;
|
||||
late String meta;
|
||||
String? tabName;
|
||||
|
||||
SpaceTabFilter.fromJson(Map<String, dynamic> json) {
|
||||
text = json['text'];
|
||||
meta = json['meta'] ?? 'all';
|
||||
tabName = json['tab_ame'];
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other is SpaceTabFilter) {
|
||||
return meta == other.meta;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => meta.hashCode;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:PiliPlus/models/space/filter.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'item.g.dart';
|
||||
@@ -56,6 +57,7 @@ class SpaceItem {
|
||||
int? iconType;
|
||||
@JsonKey(name: 'publish_time_text')
|
||||
String? publishTimeText;
|
||||
List<SpaceTabFilter>? filter;
|
||||
|
||||
SpaceItem({
|
||||
this.title,
|
||||
@@ -92,6 +94,7 @@ class SpaceItem {
|
||||
this.viewContent,
|
||||
this.iconType,
|
||||
this.publishTimeText,
|
||||
this.filter,
|
||||
});
|
||||
|
||||
factory SpaceItem.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);
|
||||
|
||||
@@ -43,6 +43,9 @@ SpaceItem _$ItemFromJson(Map<String, dynamic> json) => SpaceItem(
|
||||
viewContent: json['view_content'] as String?,
|
||||
iconType: (json['icon_type'] as num?)?.toInt(),
|
||||
publishTimeText: json['publish_time_text'] as String?,
|
||||
filter: (json['filter'] as List?)
|
||||
?.map((e) => SpaceTabFilter.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ItemToJson(SpaceItem instance) => <String, dynamic>{
|
||||
|
||||
27
lib/models/space_opus/cover.dart
Normal file
27
lib/models/space_opus/cover.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class Cover {
|
||||
int? height;
|
||||
String? url;
|
||||
int? width;
|
||||
late double ratio;
|
||||
|
||||
Cover({this.height, this.url, this.width, required this.ratio});
|
||||
|
||||
Cover.fromJson(Map<String, dynamic> json) {
|
||||
height = json['height'] as int?;
|
||||
url = json['url'] as String?;
|
||||
width = json['width'] as int?;
|
||||
if (height != null && width != null) {
|
||||
ratio = clampDouble(height! / width!, 0.68, 2.7);
|
||||
} else {
|
||||
ratio = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'height': height,
|
||||
'url': url,
|
||||
'width': width,
|
||||
};
|
||||
}
|
||||
26
lib/models/space_opus/data.dart
Normal file
26
lib/models/space_opus/data.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:PiliPlus/models/space_opus/item.dart';
|
||||
|
||||
class SpaceOpusData {
|
||||
bool? hasMore;
|
||||
List<SpaceOpusItemModel>? items;
|
||||
String? offset;
|
||||
int? updateNum;
|
||||
|
||||
SpaceOpusData({this.hasMore, this.items, this.offset, this.updateNum});
|
||||
|
||||
factory SpaceOpusData.fromJson(Map<String, dynamic> json) => SpaceOpusData(
|
||||
hasMore: json['has_more'] as bool?,
|
||||
items: (json['items'] as List<dynamic>?)
|
||||
?.map((e) => SpaceOpusItemModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
offset: json['offset'] as String?,
|
||||
updateNum: json['update_num'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'has_more': hasMore,
|
||||
'items': items?.map((e) => e.toJson()).toList(),
|
||||
'offset': offset,
|
||||
'update_num': updateNum,
|
||||
};
|
||||
}
|
||||
34
lib/models/space_opus/item.dart
Normal file
34
lib/models/space_opus/item.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:PiliPlus/models/space_opus/cover.dart';
|
||||
import 'package:PiliPlus/models/space_opus/stat.dart';
|
||||
|
||||
class SpaceOpusItemModel {
|
||||
String? content;
|
||||
String? jumpUrl;
|
||||
String? opusId;
|
||||
Stat? stat;
|
||||
Cover? cover;
|
||||
|
||||
SpaceOpusItemModel(
|
||||
{this.content, this.jumpUrl, this.opusId, this.stat, this.cover});
|
||||
|
||||
factory SpaceOpusItemModel.fromJson(Map<String, dynamic> json) =>
|
||||
SpaceOpusItemModel(
|
||||
content: json['content'] as String?,
|
||||
jumpUrl: json['jump_url'] as String?,
|
||||
opusId: json['opus_id'] as String?,
|
||||
stat: json['stat'] == null
|
||||
? null
|
||||
: Stat.fromJson(json['stat'] as Map<String, dynamic>),
|
||||
cover: json['cover'] == null
|
||||
? null
|
||||
: Cover.fromJson(json['cover'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'content': content,
|
||||
'jump_url': jumpUrl,
|
||||
'opus_id': opusId,
|
||||
'stat': stat?.toJson(),
|
||||
'cover': cover?.toJson(),
|
||||
};
|
||||
}
|
||||
13
lib/models/space_opus/stat.dart
Normal file
13
lib/models/space_opus/stat.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class Stat {
|
||||
String? like;
|
||||
|
||||
Stat({this.like});
|
||||
|
||||
factory Stat.fromJson(Map<String, dynamic> json) => Stat(
|
||||
like: json['like'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'like': like,
|
||||
};
|
||||
}
|
||||
@@ -261,13 +261,6 @@ class _MemberPageState extends State<MemberPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorWidget(msg) {
|
||||
return errorWidget(
|
||||
errMsg: msg,
|
||||
onReload: _userController.onReload,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserInfo(LoadingState userState, [bool isV = true]) {
|
||||
return switch (userState) {
|
||||
Loading() => const CircularProgressIndicator(),
|
||||
@@ -290,7 +283,10 @@ class _MemberPageState extends State<MemberPage> {
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox(height: 56, width: double.infinity),
|
||||
),
|
||||
Error() => _errorWidget(userState.errMsg),
|
||||
Error() => scrollErrorWidget(
|
||||
errMsg: userState.errMsg,
|
||||
onReload: _userController.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.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/space_article/item.dart';
|
||||
@@ -35,44 +36,51 @@ class _MemberArticleState extends State<MemberArticle>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => _buildBody(_controller.loadingState.value));
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: Obx(() => _buildBody(_controller.loadingState.value)))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<SpaceArticleItem>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => loadingWidget,
|
||||
Loading() => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return MemberArticleItem(
|
||||
item: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
? SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return MemberArticleItem(
|
||||
item: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
)
|
||||
: scrollErrorWidget(
|
||||
: HttpError(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => scrollErrorWidget(
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:PiliPlus/models/common/member/contribute_type.dart';
|
||||
import 'package:PiliPlus/pages/member_article/view.dart';
|
||||
import 'package:PiliPlus/pages/member_audio/view.dart';
|
||||
import 'package:PiliPlus/pages/member_contribute/controller.dart';
|
||||
import 'package:PiliPlus/pages/member_opus/view.dart';
|
||||
import 'package:PiliPlus/pages/member_season_series/view.dart';
|
||||
import 'package:PiliPlus/pages/member_video/view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -105,6 +106,10 @@ class _MemberContributeState extends State<MemberContribute>
|
||||
heroTag: widget.heroTag,
|
||||
mid: widget.mid,
|
||||
),
|
||||
'opus' => MemberOpus(
|
||||
heroTag: widget.heroTag,
|
||||
mid: widget.mid,
|
||||
),
|
||||
'audio' => MemberAudio(heroTag: widget.heroTag),
|
||||
'season_video' => MemberVideo(
|
||||
type: ContributeType.season,
|
||||
|
||||
@@ -58,9 +58,13 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage>
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
Obx(
|
||||
() => _buildContent(_memberDynamicController.loadingState.value),
|
||||
)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: Obx(() =>
|
||||
_buildContent(_memberDynamicController.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -69,52 +73,47 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage>
|
||||
return switch (loadingState) {
|
||||
Loading() => DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: dynamicsWaterfallFlow
|
||||
? SliverWaterfallFlow.extent(
|
||||
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
lastChildLayoutTypeBuilder: (index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_memberDynamicController.onLoadMore();
|
||||
}
|
||||
return index == loadingState.response!.length
|
||||
? LastChildLayoutType.foot
|
||||
: LastChildLayoutType.none;
|
||||
},
|
||||
children: loadingState.response!
|
||||
.map((item) => DynamicPanel(
|
||||
item: item,
|
||||
onRemove: _memberDynamicController.onRemove,
|
||||
onSetTop: _memberDynamicController.onSetTop))
|
||||
.toList(),
|
||||
)
|
||||
: SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
const SliverFillRemaining(),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: Grid.smallCardWidth * 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_memberDynamicController.onLoadMore();
|
||||
}
|
||||
return DynamicPanel(
|
||||
item: loadingState.response![index],
|
||||
onRemove: _memberDynamicController.onRemove,
|
||||
onSetTop: _memberDynamicController.onSetTop,
|
||||
);
|
||||
},
|
||||
itemCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
const SliverFillRemaining(),
|
||||
],
|
||||
? dynamicsWaterfallFlow
|
||||
? SliverWaterfallFlow.extent(
|
||||
maxCrossAxisExtent: Grid.smallCardWidth * 2,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
lastChildLayoutTypeBuilder: (index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_memberDynamicController.onLoadMore();
|
||||
}
|
||||
return index == loadingState.response!.length
|
||||
? LastChildLayoutType.foot
|
||||
: LastChildLayoutType.none;
|
||||
},
|
||||
children: loadingState.response!
|
||||
.map((item) => DynamicPanel(
|
||||
item: item,
|
||||
onRemove: _memberDynamicController.onRemove,
|
||||
onSetTop: _memberDynamicController.onSetTop))
|
||||
.toList(),
|
||||
)
|
||||
: SliverCrossAxisGroup(
|
||||
slivers: [
|
||||
const SliverFillRemaining(),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: Grid.smallCardWidth * 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_memberDynamicController.onLoadMore();
|
||||
}
|
||||
return DynamicPanel(
|
||||
item: loadingState.response![index],
|
||||
onRemove: _memberDynamicController.onRemove,
|
||||
onSetTop: _memberDynamicController.onSetTop,
|
||||
);
|
||||
},
|
||||
itemCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
)
|
||||
const SliverFillRemaining(),
|
||||
],
|
||||
)
|
||||
: HttpError(
|
||||
onReload: _memberDynamicController.onReload,
|
||||
),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.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/space_fav/datum.dart';
|
||||
import 'package:PiliPlus/models/space_fav/list.dart';
|
||||
import 'package:PiliPlus/pages/member_favorite/controller.dart';
|
||||
import 'package:PiliPlus/pages/member_favorite/widget/item.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -35,40 +37,48 @@ class _MemberFavoriteState extends State<MemberFavorite>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => _buildBody(_controller.loadingState.value));
|
||||
final theme = Theme.of(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [Obx(() => _buildBody(theme, _controller.loadingState.value))],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState loadingState) {
|
||||
final theme = Theme.of(context);
|
||||
Widget _buildBody(ThemeData theme, LoadingState loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => loadingWidget,
|
||||
Loading() => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
),
|
||||
Success() => (loadingState.response as List?)?.isNotEmpty == true
|
||||
? refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Obx(
|
||||
() => _buildItem(theme, _controller.first.value, true),
|
||||
),
|
||||
? SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Obx(
|
||||
() => _buildItem(theme, _controller.first.value, true)),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Obx(
|
||||
() => _buildItem(theme, _controller.second.value, false)),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 80 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Obx(
|
||||
() => _buildItem(theme, _controller.second.value, false),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 80 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: scrollErrorWidget(
|
||||
: HttpError(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => scrollErrorWidget(
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
|
||||
@@ -222,8 +222,8 @@ class _MemberHomeState extends State<MemberHome>
|
||||
),
|
||||
],
|
||||
)
|
||||
: errorWidget(),
|
||||
Error() => errorWidget(),
|
||||
: scrollErrorWidget(),
|
||||
Error() => scrollErrorWidget(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
59
lib/pages/member_opus/controller.dart
Normal file
59
lib/pages/member_opus/controller.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/member.dart';
|
||||
import 'package:PiliPlus/models/space/filter.dart';
|
||||
import 'package:PiliPlus/models/space_opus/data.dart';
|
||||
import 'package:PiliPlus/models/space_opus/item.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:PiliPlus/pages/member/controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MemberOpusController
|
||||
extends CommonListController<SpaceOpusData, SpaceOpusItemModel> {
|
||||
MemberOpusController({
|
||||
required this.heroTag,
|
||||
required this.mid,
|
||||
});
|
||||
|
||||
final String? heroTag;
|
||||
final int mid;
|
||||
|
||||
String offset = '';
|
||||
Rx<SpaceTabFilter> type =
|
||||
SpaceTabFilter(text: "全部图文", meta: "all", tabName: "图文").obs;
|
||||
List<SpaceTabFilter>? filter;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
filter = Get.find<MemberController>(tag: heroTag)
|
||||
.tab2
|
||||
?.firstWhereOrNull((e) => e.param == 'contribute')
|
||||
?.items
|
||||
?.firstWhereOrNull((e) => e.param == 'opus')
|
||||
?.filter;
|
||||
queryData();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onRefresh() {
|
||||
offset = '';
|
||||
return super.onRefresh();
|
||||
}
|
||||
|
||||
@override
|
||||
List<SpaceOpusItemModel>? getDataList(SpaceOpusData response) {
|
||||
offset = response.offset ?? '';
|
||||
if (response.hasMore == false) {
|
||||
isEnd = true;
|
||||
}
|
||||
return response.items;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<SpaceOpusData>> customGetData() => MemberHttp.spaceOpus(
|
||||
hostMid: mid,
|
||||
page: currentPage,
|
||||
offset: offset,
|
||||
type: type.value.meta,
|
||||
);
|
||||
}
|
||||
154
lib/pages/member_opus/view.dart
Normal file
154
lib/pages/member_opus/view.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/space_opus.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/space_opus/item.dart';
|
||||
import 'package:PiliPlus/pages/member_opus/controller.dart';
|
||||
import 'package:PiliPlus/pages/member_opus/widgets/space_opus_item.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:waterfall_flow/waterfall_flow.dart';
|
||||
|
||||
class MemberOpus extends StatefulWidget {
|
||||
const MemberOpus({
|
||||
super.key,
|
||||
required this.heroTag,
|
||||
required this.mid,
|
||||
});
|
||||
|
||||
final String? heroTag;
|
||||
final int mid;
|
||||
|
||||
@override
|
||||
State<MemberOpus> createState() => _MemberOpusState();
|
||||
}
|
||||
|
||||
class _MemberOpusState extends State<MemberOpus>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late final _controller = Get.put(
|
||||
MemberOpusController(
|
||||
mid: widget.mid,
|
||||
heroTag: widget.heroTag,
|
||||
),
|
||||
tag: widget.heroTag,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Stack(
|
||||
children: [
|
||||
refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: StyleString.safeSpace,
|
||||
right: StyleString.safeSpace,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 90,
|
||||
),
|
||||
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_controller.filter?.isNotEmpty == true)
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 16,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _controller.filter!
|
||||
.map(
|
||||
(e) => ListTile(
|
||||
onTap: () {
|
||||
if (e == _controller.type.value) {
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
_controller
|
||||
..type.value = e
|
||||
..onReload();
|
||||
},
|
||||
tileColor: e == _controller.type.value
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface
|
||||
: null,
|
||||
dense: true,
|
||||
title: Text(
|
||||
e.text ?? e.tabName!,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
label: Row(
|
||||
children: [
|
||||
const Icon(size: 20, Icons.sort),
|
||||
Obx(
|
||||
() => Text(_controller.type.value.text ??
|
||||
_controller.type.value.tabName!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<SpaceOpusItemModel>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => SliverWaterfallFlow.extent(
|
||||
maxCrossAxisExtent: Grid.smallCardWidth,
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
children: List.generate(10, (_) => const SpaceOpusSkeleton()),
|
||||
),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? SliverWaterfallFlow.extent(
|
||||
maxCrossAxisExtent: Grid.smallCardWidth,
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
lastChildLayoutTypeBuilder: (index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return index == loadingState.response!.length
|
||||
? LastChildLayoutType.foot
|
||||
: LastChildLayoutType.none;
|
||||
},
|
||||
children: loadingState.response!
|
||||
.map((item) => SpaceOpusItem(item: item))
|
||||
.toList(),
|
||||
)
|
||||
: HttpError(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
95
lib/pages/member_opus/widgets/space_opus_item.dart
Normal file
95
lib/pages/member_opus/widgets/space_opus_item.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/models/space_opus/item.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SpaceOpusItem extends StatelessWidget {
|
||||
const SpaceOpusItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
});
|
||||
|
||||
final SpaceOpusItemModel item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasPic = item.cover?.url?.isNotEmpty == true;
|
||||
return Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
PageUtils.pushDynFromId(id: item.opusId!);
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasPic)
|
||||
Stack(
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return NetworkImgLayer(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth * item.cover!.ratio,
|
||||
src: item.cover!.url,
|
||||
type: 'emote',
|
||||
quality: 60,
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 45,
|
||||
alignment: Alignment.bottomLeft,
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 4),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black54],
|
||||
),
|
||||
),
|
||||
child: StatView(
|
||||
context: context,
|
||||
value: item.stat?.like ?? 0,
|
||||
goto: 'like',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.content?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(
|
||||
item.content!,
|
||||
maxLines: hasPic ? 4 : 6,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (!hasPic)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 8, right: 8),
|
||||
child: StatView(
|
||||
context: context,
|
||||
value: item.stat?.like ?? 0,
|
||||
goto: 'like',
|
||||
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.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/space_archive/item.dart';
|
||||
@@ -39,55 +39,54 @@ class _MemberBangumiState extends State<MemberBangumi>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => _buildBody(_controller.loadingState.value));
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: StyleString.safeSpace,
|
||||
right: StyleString.safeSpace,
|
||||
top: StyleString.safeSpace,
|
||||
bottom: StyleString.safeSpace +
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
80,
|
||||
),
|
||||
sliver: Obx(
|
||||
() => _buildBody(_controller.loadingState.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<SpaceArchiveItem>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => loadingWidget,
|
||||
Loading() => const SliverToBoxAdapter(),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: StyleString.safeSpace,
|
||||
right: StyleString.safeSpace,
|
||||
top: StyleString.safeSpace,
|
||||
bottom: StyleString.safeSpace +
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
80,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
mainAxisSpacing: StyleString.cardSpace,
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2,
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisExtent:
|
||||
MediaQuery.textScalerOf(context).scale(52),
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return BangumiCardVMemberHome(
|
||||
bangumiItem: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
? SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithExtentAndRatio(
|
||||
mainAxisSpacing: StyleString.cardSpace,
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2,
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisExtent: MediaQuery.textScalerOf(context).scale(52),
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return BangumiCardVMemberHome(
|
||||
bangumiItem: loadingState.response![index],
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
)
|
||||
: scrollErrorWidget(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => scrollErrorWidget(
|
||||
: HttpError(onReload: _controller.onReload),
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
|
||||
@@ -90,124 +90,122 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
|
||||
return switch (loadingState) {
|
||||
Loading() => loadingWidget,
|
||||
Success() => SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '头像',
|
||||
widget: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl:
|
||||
Utils.thumbnailImgUrl(loadingState.response['face']),
|
||||
),
|
||||
Success() => ListView(
|
||||
children: [
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '头像',
|
||||
widget: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl:
|
||||
Utils.thumbnailImgUrl(loadingState.response['face']),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
EasyThrottle.throttle(
|
||||
'imagePicker', const Duration(milliseconds: 500), () {
|
||||
_pickImg(theme);
|
||||
});
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '昵称',
|
||||
text: loadingState.response['name'],
|
||||
onTap: () {
|
||||
if (loadingState.response['coins'] < 6) {
|
||||
SmartDialog.showToast('硬币不足');
|
||||
} else {
|
||||
_editDialog(
|
||||
type: ProfileType.uname,
|
||||
title: '昵称',
|
||||
text: loadingState.response['name'],
|
||||
onTap: () {
|
||||
EasyThrottle.throttle(
|
||||
'imagePicker', const Duration(milliseconds: 500), () {
|
||||
_pickImg(theme);
|
||||
});
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '昵称',
|
||||
text: loadingState.response['name'],
|
||||
onTap: () {
|
||||
if (loadingState.response['coins'] < 6) {
|
||||
SmartDialog.showToast('硬币不足');
|
||||
} else {
|
||||
_editDialog(
|
||||
type: ProfileType.uname,
|
||||
title: '昵称',
|
||||
text: loadingState.response['name'],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '性别',
|
||||
text: _sex(loadingState.response['sex']),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context_) =>
|
||||
_sexDialog(loadingState.response['sex']),
|
||||
);
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '出生年月',
|
||||
text: loadingState.response['birthday'],
|
||||
onTap: () {
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
DateTime.parse(loadingState.response['birthday']),
|
||||
firstDate: DateTime(1900, 1, 1),
|
||||
lastDate: DateTime.now(),
|
||||
).then((date) {
|
||||
if (date != null) {
|
||||
_update(
|
||||
type: ProfileType.birthday,
|
||||
datum: DateFormat('yyyy-MM-dd').format(date),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '性别',
|
||||
text: _sex(loadingState.response['sex']),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context_) =>
|
||||
_sexDialog(loadingState.response['sex']),
|
||||
);
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '出生年月',
|
||||
text: loadingState.response['birthday'],
|
||||
onTap: () {
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
DateTime.parse(loadingState.response['birthday']),
|
||||
firstDate: DateTime(1900, 1, 1),
|
||||
lastDate: DateTime.now(),
|
||||
).then((date) {
|
||||
if (date != null) {
|
||||
_update(
|
||||
type: ProfileType.birthday,
|
||||
datum: DateFormat('yyyy-MM-dd').format(date),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '个性签名',
|
||||
text: loadingState.response['sign'].isEmpty
|
||||
? '无'
|
||||
: loadingState.response['sign'],
|
||||
onTap: () {
|
||||
_editDialog(
|
||||
type: ProfileType.sign,
|
||||
title: '个性签名',
|
||||
text: loadingState.response['sign'],
|
||||
);
|
||||
},
|
||||
),
|
||||
divider1,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '头像挂件',
|
||||
onTap: () => PageUtils.launchURL(
|
||||
'https://www.bilibili.com/h5/mall/pendant/home'),
|
||||
),
|
||||
divider1,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: 'UID',
|
||||
needIcon: false,
|
||||
text: loadingState.response['mid'].toString(),
|
||||
onTap: () =>
|
||||
Utils.copyText(loadingState.response['mid'].toString()),
|
||||
),
|
||||
divider1,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '哔哩哔哩认证',
|
||||
onTap: () => PageUtils.launchURL(
|
||||
'https://account.bilibili.com/official/mobile/home'),
|
||||
),
|
||||
divider,
|
||||
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
),
|
||||
});
|
||||
},
|
||||
),
|
||||
divider,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '个性签名',
|
||||
text: loadingState.response['sign'].isEmpty
|
||||
? '无'
|
||||
: loadingState.response['sign'],
|
||||
onTap: () {
|
||||
_editDialog(
|
||||
type: ProfileType.sign,
|
||||
title: '个性签名',
|
||||
text: loadingState.response['sign'],
|
||||
);
|
||||
},
|
||||
),
|
||||
divider1,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '头像挂件',
|
||||
onTap: () => PageUtils.launchURL(
|
||||
'https://www.bilibili.com/h5/mall/pendant/home'),
|
||||
),
|
||||
divider1,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: 'UID',
|
||||
needIcon: false,
|
||||
text: loadingState.response['mid'].toString(),
|
||||
onTap: () =>
|
||||
Utils.copyText(loadingState.response['mid'].toString()),
|
||||
),
|
||||
divider1,
|
||||
_item(
|
||||
theme: theme,
|
||||
title: '哔哩哔哩认证',
|
||||
onTap: () => PageUtils.launchURL(
|
||||
'https://account.bilibili.com/official/mobile/home'),
|
||||
),
|
||||
divider,
|
||||
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
),
|
||||
Error() => errorWidget(
|
||||
Error() => scrollErrorWidget(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _getInfo,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/member/contribute_type.dart';
|
||||
import 'package:PiliPlus/pages/member_season_series/controller.dart';
|
||||
@@ -36,75 +37,81 @@ class _SeasonSeriesPageState extends State<SeasonSeriesPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => _buildBody(_controller.loadingState.value));
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: StyleString.safeSpace - 5,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: Obx(
|
||||
() => _buildBody(_controller.loadingState.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<dynamic>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => loadingWidget,
|
||||
Loading() => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: StyleString.safeSpace - 5,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
dynamic item = loadingState.response![index];
|
||||
return SeasonSeriesCard(
|
||||
item: item,
|
||||
onTap: () {
|
||||
bool isSeason = item['meta']['season_id'] != null;
|
||||
dynamic id = isSeason
|
||||
? item['meta']['season_id']
|
||||
: item['meta']['series_id'];
|
||||
Get.to(
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(item['meta']['name']),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: MemberVideo(
|
||||
type: isSeason
|
||||
? ContributeType.season
|
||||
: ContributeType.series,
|
||||
heroTag: widget.heroTag,
|
||||
mid: widget.mid,
|
||||
seasonId: isSeason ? id : null,
|
||||
seriesId: isSeason ? null : id,
|
||||
title: item['meta']['name'],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
? SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
dynamic item = loadingState.response![index];
|
||||
return SeasonSeriesCard(
|
||||
item: item,
|
||||
onTap: () {
|
||||
bool isSeason = item['meta']['season_id'] != null;
|
||||
dynamic id = isSeason
|
||||
? item['meta']['season_id']
|
||||
: item['meta']['series_id'];
|
||||
Get.to(
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(item['meta']['name']),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: MemberVideo(
|
||||
type: isSeason
|
||||
? ContributeType.season
|
||||
: ContributeType.series,
|
||||
heroTag: widget.heroTag,
|
||||
mid: widget.mid,
|
||||
seasonId: isSeason ? id : null,
|
||||
seriesId: isSeason ? null : id,
|
||||
title: item['meta']['name'],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
)
|
||||
: scrollErrorWidget(
|
||||
onReload: () {
|
||||
_controller.onReload();
|
||||
},
|
||||
: HttpError(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => scrollErrorWidget(
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: () {
|
||||
_controller.onReload();
|
||||
},
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_card/video_card_h_member_video.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/member/contribute_type.dart';
|
||||
@@ -56,170 +55,176 @@ class _MemberVideoState extends State<MemberVideo>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => _buildBody(_controller.loadingState.value));
|
||||
final theme = Theme.of(context);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
// physics: PositionRetainedScrollPhysics(
|
||||
// shouldRetain: _controller.isLocating == true,
|
||||
// parent: const ClampingScrollPhysics(),
|
||||
// ),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
sliver: Obx(
|
||||
() => _buildBody(theme, _controller.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.type == ContributeType.video &&
|
||||
_controller.fromViewAid?.isNotEmpty == true &&
|
||||
_controller.isLocating != true)
|
||||
Positioned(
|
||||
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();
|
||||
},
|
||||
label: const Text('定位至上次观看'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<SpaceArchiveItem>?> loadingState) {
|
||||
final theme = Theme.of(context);
|
||||
Widget _buildBody(
|
||||
ThemeData theme, LoadingState<List<SpaceArchiveItem>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => loadingWidget,
|
||||
Loading() => SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
),
|
||||
Success() => loadingState.response?.isNotEmpty == true
|
||||
? Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
physics: PositionRetainedScrollPhysics(
|
||||
shouldRetain: _controller.isLocating == true,
|
||||
parent: const ClampingScrollPhysics(),
|
||||
),
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: false,
|
||||
floating: true,
|
||||
delegate: CustomSliverPersistentHeaderDelegate(
|
||||
extent: 40,
|
||||
bgColor: theme.colorScheme.surface,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Obx(
|
||||
() => Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Text(
|
||||
_controller.count.value != -1
|
||||
? '共${_controller.count.value}视频'
|
||||
: '',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => _controller.episodicButton.value.uri !=
|
||||
null
|
||||
? Container(
|
||||
height: 35,
|
||||
padding: EdgeInsets.only(
|
||||
left:
|
||||
_controller.count.value != -1
|
||||
? 6
|
||||
: 0),
|
||||
child: TextButton.icon(
|
||||
onPressed:
|
||||
_controller.toViewPlayAll,
|
||||
icon: Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
size: 16,
|
||||
color:
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
label: Text(
|
||||
_controller.episodicButton.value
|
||||
.text ??
|
||||
'播放全部',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color:
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: _controller.queryBySort,
|
||||
icon: Icon(
|
||||
Icons.sort,
|
||||
size: 16,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
label: Obx(
|
||||
() => Text(
|
||||
widget.type == ContributeType.video
|
||||
? _controller.order.value ==
|
||||
'pubdate'
|
||||
? '最新发布'
|
||||
: '最多播放'
|
||||
: _controller.sort.value == 'desc'
|
||||
? '默认'
|
||||
: '倒序',
|
||||
? SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: false,
|
||||
floating: true,
|
||||
delegate: CustomSliverPersistentHeaderDelegate(
|
||||
extent: 40,
|
||||
bgColor: theme.colorScheme.surface,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
Obx(
|
||||
() => Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Text(
|
||||
_controller.count.value != -1
|
||||
? '共${_controller.count.value}视频'
|
||||
: '',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => _controller.episodicButton.value.uri != null
|
||||
? Container(
|
||||
height: 35,
|
||||
padding: EdgeInsets.only(
|
||||
left: _controller.count.value != -1
|
||||
? 6
|
||||
: 0),
|
||||
child: TextButton.icon(
|
||||
onPressed: _controller.toViewPlayAll,
|
||||
icon: Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
size: 16,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
label: Text(
|
||||
_controller.episodicButton.value.text ??
|
||||
'播放全部',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: _controller.queryBySort,
|
||||
icon: Icon(
|
||||
Icons.sort,
|
||||
size: 16,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
label: Obx(
|
||||
() => Text(
|
||||
widget.type == ContributeType.video
|
||||
? _controller.order.value == 'pubdate'
|
||||
? '最新发布'
|
||||
: '最多播放'
|
||||
: _controller.sort.value == 'desc'
|
||||
? '默认'
|
||||
: '倒序',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: StyleString.safeSpace - 5,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (widget.type != ContributeType.season &&
|
||||
index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
final SpaceArchiveItem item =
|
||||
loadingState.response![index];
|
||||
return VideoCardHMemberVideo(
|
||||
key: ValueKey('${item.param}'),
|
||||
videoItem: item,
|
||||
fromViewAid: _controller.fromViewAid,
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.type == ContributeType.video &&
|
||||
_controller.fromViewAid?.isNotEmpty == true &&
|
||||
_controller.isLocating != true)
|
||||
Positioned(
|
||||
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();
|
||||
},
|
||||
label: const Text('定位至上次观看'),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (widget.type != ContributeType.season &&
|
||||
index == loadingState.response!.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
final SpaceArchiveItem item =
|
||||
loadingState.response![index];
|
||||
return VideoCardHMemberVideo(
|
||||
key: ValueKey('${item.param}'),
|
||||
videoItem: item,
|
||||
fromViewAid: _controller.fromViewAid,
|
||||
);
|
||||
},
|
||||
childCount: loadingState.response!.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: scrollErrorWidget(
|
||||
: HttpError(
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
Error() => scrollErrorWidget(
|
||||
Error() => HttpError(
|
||||
errMsg: loadingState.errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
|
||||
@@ -102,12 +102,12 @@ class _WhisperDetailPageState
|
||||
children: [
|
||||
Expanded(
|
||||
child: Listener(
|
||||
child: Obx(() =>
|
||||
_buildBody(_whisperDetailController.loadingState.value)),
|
||||
onPointerDown: (event) {
|
||||
// Hide panel when touch ListView.
|
||||
hidePanel();
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Obx(() =>
|
||||
_buildBody(_whisperDetailController.loadingState.value)),
|
||||
),
|
||||
),
|
||||
_buildInputView(theme),
|
||||
|
||||
Reference in New Issue
Block a user