merge main
This commit is contained in:
@ -184,7 +184,7 @@ class AboutController extends GetxController {
|
||||
|
||||
// 获取远程版本
|
||||
Future getRemoteApp() async {
|
||||
var result = await Request().get(Api.latestApp);
|
||||
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
|
||||
data = LatestDataModel.fromJson(result.data);
|
||||
remoteAppInfo = data;
|
||||
remoteVersion.value = data.tagName!;
|
||||
|
||||
@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
|
||||
? int.parse(Get.parameters['seasonId']!)
|
||||
: null;
|
||||
var epId = Get.parameters['epId'] != null
|
||||
? int.parse(Get.parameters['epId']!)
|
||||
? int.tryParse(Get.parameters['epId']!)
|
||||
: null;
|
||||
|
||||
// 是否预渲染 骨架屏
|
||||
@ -257,7 +258,7 @@ class BangumiIntroController extends GetxController {
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
videoDetailCtr.bvid = bvid;
|
||||
videoDetailCtr.cid = cid;
|
||||
videoDetailCtr.cid.value = cid;
|
||||
videoDetailCtr.danmakuCid.value = cid;
|
||||
videoDetailCtr.queryVideoUrl();
|
||||
// 重新请求评论
|
||||
@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 列表循环或者顺序播放时,自动播放下一个
|
||||
void nextPlay() {
|
||||
late List episodes;
|
||||
if (bangumiDetail.value.episodes != null) {
|
||||
episodes = bangumiDetail.value.episodes!;
|
||||
}
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
int currentIndex =
|
||||
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
|
||||
int nextIndex = currentIndex + 1;
|
||||
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
|
||||
// 列表循环
|
||||
if (platRepeat == PlayRepeat.listCycle) {
|
||||
if (nextIndex == episodes.length - 1) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
}
|
||||
if (nextIndex <= episodes.length - 1 &&
|
||||
platRepeat == PlayRepeat.listOrder) {}
|
||||
|
||||
int cid = episodes[nextIndex].cid!;
|
||||
String bvid = episodes[nextIndex].bvid!;
|
||||
int aid = episodes[nextIndex].aid!;
|
||||
changeSeasonOrbangu(bvid, cid, aid);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
|
||||
|
||||
class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final BangumiIntroController bangumiIntroController =
|
||||
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
|
||||
late BangumiIntroController bangumiIntroController;
|
||||
late VideoDetailController videoDetailCtr;
|
||||
BangumiInfoModel? bangumiDetail;
|
||||
late Future _futureBuilderFuture;
|
||||
late int cid;
|
||||
late String heroTag;
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
cid = widget.cid!;
|
||||
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
bangumiIntroController.bangumiDetail.listen((value) {
|
||||
bangumiDetail = value;
|
||||
});
|
||||
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
|
||||
videoDetailCtr.cid.listen((p0) {
|
||||
print('🐶🐶$p0');
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -61,22 +72,25 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
|
||||
return BangumiInfo(
|
||||
loadingStatus: false,
|
||||
bangumiDetail: bangumiDetail,
|
||||
cid: cid,
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: snapshot.data['msg'],
|
||||
fn: () => Get.back(),
|
||||
);
|
||||
// return HttpError(
|
||||
// errMsg: snapshot.data['msg'],
|
||||
// fn: () => Get.back(),
|
||||
// );
|
||||
return SizedBox();
|
||||
}
|
||||
} else {
|
||||
return BangumiInfo(
|
||||
loadingStatus: true,
|
||||
bangumiDetail: bangumiDetail,
|
||||
cid: widget.cid,
|
||||
cid: cid,
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -117,6 +131,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
bangumiItem = bangumiIntroController.bangumiItem;
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
cid = widget.cid!;
|
||||
print('cid: $cid');
|
||||
videoDetailCtr.cid.listen((p0) {
|
||||
cid = p0;
|
||||
print('cid: $cid');
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
// 收藏
|
||||
@ -260,9 +280,15 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
children: [
|
||||
Text(
|
||||
!widget.loadingStatus
|
||||
? widget.bangumiDetail!.areas!
|
||||
.first['name']
|
||||
: bangumiItem!.areas!.first['name'],
|
||||
? (widget.bangumiDetail!.areas!
|
||||
.isNotEmpty
|
||||
? widget.bangumiDetail!.areas!
|
||||
.first['name']
|
||||
: '')
|
||||
: (bangumiItem!.areas!.isNotEmpty
|
||||
? bangumiItem!
|
||||
.areas!.first['name']
|
||||
: ''),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
|
||||
@ -113,6 +113,9 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
List list = _bangumidController.bangumiFollowList;
|
||||
if (data['status']) {
|
||||
@ -198,7 +201,7 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
},
|
||||
),
|
||||
),
|
||||
const LoadingMore()
|
||||
LoadingMore()
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class BangumiPanel extends StatefulWidget {
|
||||
@ -30,16 +32,28 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
||||
dynamic userInfo;
|
||||
// 默认未开通
|
||||
int vipStatus = 0;
|
||||
late int cid;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late final VideoDetailController videoDetailCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
|
||||
cid = widget.cid!;
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo != null) {
|
||||
vipStatus = userInfo.vipStatus;
|
||||
}
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
|
||||
videoDetailCtr.cid.listen((p0) {
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
centerTitle: false,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'黑名单管理 (${_blackListController.blackList.length} / 5000)',
|
||||
'黑名单管理 - ${_blackListController.total.value}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
dense: true,
|
||||
// trailing: TextButton(
|
||||
// onPressed: () {},
|
||||
// child: const Text('移除'),
|
||||
// ),
|
||||
trailing: TextButton(
|
||||
onPressed: () => _blackListController
|
||||
.removeBlack(list[index].mid),
|
||||
child: const Text('移除'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
class BlackListController extends GetxController {
|
||||
int currentPage = 1;
|
||||
int pageSize = 50;
|
||||
RxInt total = 0.obs;
|
||||
RxList<BlackListItem> blackList = [BlackListItem()].obs;
|
||||
|
||||
Future queryBlacklist({type = 'init'}) async {
|
||||
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
|
||||
if (result['status']) {
|
||||
if (type == 'init') {
|
||||
blackList.value = result['data'].list;
|
||||
total.value = result['data'].total;
|
||||
} else {
|
||||
blackList.addAll(result['data'].list);
|
||||
}
|
||||
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future removeBlack(mid) async {
|
||||
var result = await BlackHttp.removeBlack(fid: mid);
|
||||
if (result['status']) {
|
||||
blackList.removeWhere((e) => e.mid == mid);
|
||||
total.value = total.value - 1;
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,22 +10,34 @@ class PlDanmakuController {
|
||||
// 按 6min 分段
|
||||
int segCount = 0;
|
||||
List<DmSegMobileReply> dmSegList = [];
|
||||
int currentSegIndex = 0;
|
||||
// 已请求的段落标记
|
||||
List<int> hasrequestSeg = [];
|
||||
int currentSegIndex = 1;
|
||||
int currentDmIndex = 0;
|
||||
|
||||
void calcSegment() {
|
||||
dmSegList.clear();
|
||||
// 视频分段数
|
||||
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
|
||||
dmSegList = List<DmSegMobileReply>.generate(
|
||||
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
|
||||
// 当前分段
|
||||
try {
|
||||
currentSegIndex =
|
||||
(playerController.position.value.inSeconds / (60 * 6)).ceil();
|
||||
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<List<DmSegMobileReply>> queryDanmaku() async {
|
||||
dmSegList.clear();
|
||||
for (int segIndex = 1; segIndex <= segCount; segIndex++) {
|
||||
DmSegMobileReply result =
|
||||
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segIndex);
|
||||
if (result.elems.isNotEmpty) {
|
||||
result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
|
||||
dmSegList.add(result);
|
||||
}
|
||||
// dmSegList.clear();
|
||||
DmSegMobileReply result =
|
||||
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex);
|
||||
if (result.elems.isNotEmpty) {
|
||||
result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
|
||||
// dmSegList.add(result);
|
||||
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
|
||||
dmSegList[currentSegIndex - 1] = result;
|
||||
}
|
||||
if (dmSegList.isNotEmpty) {
|
||||
findClosestPositionIndex(playerController.position.value.inMilliseconds);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
@ -29,6 +30,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
bool danmuPlayStatus = true;
|
||||
Box setting = GStrorage.setting;
|
||||
late bool enableShowDanmaku;
|
||||
late List blockTypes;
|
||||
late double showArea;
|
||||
late double opacityVal;
|
||||
late double fontSizeVal;
|
||||
late double danmakuSpeedVal;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -58,6 +64,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
}
|
||||
}
|
||||
});
|
||||
blockTypes = playerController.blockTypes;
|
||||
showArea = playerController.showArea;
|
||||
opacityVal = playerController.opacityVal;
|
||||
fontSizeVal = playerController.fontSizeVal;
|
||||
danmakuSpeedVal = playerController.danmakuSpeedVal;
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
@ -75,12 +86,23 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
_controller!.onResume();
|
||||
danmuPlayStatus = true;
|
||||
}
|
||||
PlDanmakuController ctr = _plDanmakuController;
|
||||
int currentPosition = position.inMilliseconds;
|
||||
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
return;
|
||||
}
|
||||
PlDanmakuController ctr = _plDanmakuController;
|
||||
int currentPosition = position.inMilliseconds;
|
||||
blockTypes = playerController.blockTypes;
|
||||
// 根据position判断是否有已缓存弹幕。没有则请求对应段
|
||||
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
|
||||
segIndex = segIndex < 1 ? 1 : segIndex;
|
||||
if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
|
||||
!ctr.hasrequestSeg.contains(segIndex - 1)) {
|
||||
ctr.hasrequestSeg.add(segIndex - 1);
|
||||
ctr.currentSegIndex = segIndex;
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
ctr.queryDanmaku();
|
||||
});
|
||||
}
|
||||
// 超出分段数返回
|
||||
if (ctr.currentSegIndex >= ctr.dmSegList.length) {
|
||||
return;
|
||||
@ -99,14 +121,17 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
var delta = currentPosition - element.progress;
|
||||
|
||||
if (delta >= 0 && delta < 200) {
|
||||
_controller!.addItems([
|
||||
DanmakuItem(
|
||||
element.content,
|
||||
color: DmUtils.decimalToColor(element.color),
|
||||
time: element.progress,
|
||||
type: DmUtils.getPosition(element.mode),
|
||||
)
|
||||
]);
|
||||
// 屏蔽彩色弹幕
|
||||
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
|
||||
_controller!.addItems([
|
||||
DanmakuItem(
|
||||
element.content,
|
||||
color: DmUtils.decimalToColor(element.color),
|
||||
time: element.progress,
|
||||
type: DmUtils.getPosition(element.mode),
|
||||
)
|
||||
]);
|
||||
}
|
||||
ctr.currentDmIndex++;
|
||||
} else {
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
@ -126,22 +151,30 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => AnimatedOpacity(
|
||||
opacity: playerController.isOpenDanmu.value ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: DanmakuView(
|
||||
createdController: (DanmakuController e) async {
|
||||
widget.playerController.danmakuController = _controller = e;
|
||||
},
|
||||
option: DanmakuOption(
|
||||
fontSize: 15,
|
||||
area: 0.5,
|
||||
duration: 5,
|
||||
return LayoutBuilder(builder: (context, box) {
|
||||
double initDuration = box.maxWidth / 12;
|
||||
return Obx(
|
||||
() => AnimatedOpacity(
|
||||
opacity: playerController.isOpenDanmu.value ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: DanmakuView(
|
||||
createdController: (DanmakuController e) async {
|
||||
widget.playerController.danmakuController = _controller = e;
|
||||
},
|
||||
option: DanmakuOption(
|
||||
fontSize: 15 * fontSizeVal,
|
||||
area: showArea,
|
||||
opacity: opacityVal,
|
||||
hideTop: blockTypes.contains(5),
|
||||
hideScroll: blockTypes.contains(2),
|
||||
hideBottom: blockTypes.contains(4),
|
||||
duration: initDuration /
|
||||
(danmakuSpeedVal * widget.playerController.playbackSpeed),
|
||||
),
|
||||
statusChanged: (isPlaying) {},
|
||||
),
|
||||
statusChanged: (isPlaying) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,10 +149,30 @@ class DynamicsController extends GetxController {
|
||||
case 'DYNAMIC_TYPE_ARTICLE':
|
||||
String title = item.modules.moduleDynamic.major.opus.title;
|
||||
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title},
|
||||
);
|
||||
if (url.contains('opus') || url.contains('read')) {
|
||||
RegExp digitRegExp = RegExp(r'\d+');
|
||||
Iterable<Match> matches = digitRegExp.allMatches(url);
|
||||
String number = matches.first.group(0)!;
|
||||
if (url.contains('read')) {
|
||||
number = 'cv$number';
|
||||
}
|
||||
Get.toNamed('/htmlRender', parameters: {
|
||||
'url': url.startsWith('//') ? url.split('//').last : url,
|
||||
'title': title,
|
||||
'id': number,
|
||||
'dynamicType': url.split('//').last.split('/')[1]
|
||||
});
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': 'https:$url',
|
||||
'type': 'note',
|
||||
'pageTitle': title
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'DYNAMIC_TYPE_PGC':
|
||||
print('番剧');
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/reply.dart';
|
||||
@ -17,6 +18,7 @@ class DynamicDetailController extends GetxController {
|
||||
RxString noMore = ''.obs;
|
||||
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
|
||||
RxInt acount = 0.obs;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
ReplySortType _sortType = ReplySortType.time;
|
||||
RxString sortTypeTitle = ReplySortType.time.titles.obs;
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_reply.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
@ -9,7 +10,10 @@ import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/pages/dynamics/deatil/index.dart';
|
||||
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
|
||||
import '../widgets/dynamic_panel.dart';
|
||||
|
||||
@ -21,15 +25,18 @@ class DynamicDetailPage extends StatefulWidget {
|
||||
State<DynamicDetailPage> createState() => _DynamicDetailPageState();
|
||||
}
|
||||
|
||||
class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
late DynamicDetailController? _dynamicDetailController;
|
||||
class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
with TickerProviderStateMixin {
|
||||
late DynamicDetailController _dynamicDetailController;
|
||||
late AnimationController fabAnimationCtr;
|
||||
Future? _futureBuilderFuture;
|
||||
late StreamController<bool> titleStreamC; // appBar title
|
||||
final ScrollController scrollController = ScrollController();
|
||||
late ScrollController scrollController;
|
||||
bool _visibleTitle = false;
|
||||
String? action;
|
||||
// 回复类型
|
||||
late int type;
|
||||
bool _isFabVisible = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -38,39 +45,42 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
// floor 1原创 2转发
|
||||
if (Get.arguments['floor'] == 1) {
|
||||
oid = int.parse(Get.arguments['item'].basic!['comment_id_str']);
|
||||
print(oid);
|
||||
} else {
|
||||
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
|
||||
try {
|
||||
String type = Get.arguments['item'].modules.moduleDynamic.major.type;
|
||||
|
||||
/// TODO
|
||||
if (type == 'MAJOR_TYPE_OPUS') {
|
||||
} else {
|
||||
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
|
||||
int commentType = 11;
|
||||
try {
|
||||
commentType = Get.arguments['item'].basic!['comment_type'];
|
||||
} catch (_) {}
|
||||
type = (commentType == 0) ? 11 : commentType;
|
||||
|
||||
action =
|
||||
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
|
||||
_dynamicDetailController = Get.put(DynamicDetailController(oid, type));
|
||||
_futureBuilderFuture = _dynamicDetailController!.queryReplyList();
|
||||
_dynamicDetailController =
|
||||
Get.put(DynamicDetailController(oid, type), tag: oid.toString());
|
||||
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
|
||||
titleStreamC = StreamController<bool>();
|
||||
scrollController.addListener(_listen);
|
||||
if (action == 'comment') {
|
||||
_visibleTitle = true;
|
||||
titleStreamC.add(true);
|
||||
}
|
||||
}
|
||||
|
||||
void _listen() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_dynamicDetailController!.queryReplyList(reqType: 'onLoad');
|
||||
});
|
||||
}
|
||||
|
||||
if (scrollController.offset > 55 && !_visibleTitle) {
|
||||
_visibleTitle = true;
|
||||
titleStreamC.add(true);
|
||||
} else if (scrollController.offset <= 55 && _visibleTitle) {
|
||||
_visibleTitle = false;
|
||||
titleStreamC.add(false);
|
||||
}
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
fabAnimationCtr.forward();
|
||||
// 滚动事件监听
|
||||
scrollListener();
|
||||
}
|
||||
|
||||
void replyReply(replyItem) {
|
||||
@ -97,9 +107,58 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void scrollListener() {
|
||||
scrollController = _dynamicDetailController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
// 分页加载
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_dynamicDetailController.queryReplyList(reqType: 'onLoad');
|
||||
});
|
||||
}
|
||||
|
||||
// 标题
|
||||
if (scrollController.offset > 55 && !_visibleTitle) {
|
||||
_visibleTitle = true;
|
||||
titleStreamC.add(true);
|
||||
} else if (scrollController.offset <= 55 && _visibleTitle) {
|
||||
_visibleTitle = false;
|
||||
titleStreamC.add(false);
|
||||
}
|
||||
|
||||
// fab按钮
|
||||
final ScrollDirection direction =
|
||||
scrollController.position.userScrollDirection;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
_showFab();
|
||||
} else if (direction == ScrollDirection.reverse) {
|
||||
_hideFab();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showFab() {
|
||||
if (!_isFabVisible) {
|
||||
_isFabVisible = true;
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _hideFab() {
|
||||
if (_isFabVisible) {
|
||||
_isFabVisible = false;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
fabAnimationCtr.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -118,7 +177,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
return AnimatedOpacity(
|
||||
opacity: snapshot.data ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: author(_dynamicDetailController!.item, context),
|
||||
child: AuthorPanel(item: _dynamicDetailController.item),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -126,155 +185,206 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _dynamicDetailController!.queryReplyList();
|
||||
await _dynamicDetailController.queryReplyList();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
if (action != 'comment')
|
||||
SliverToBoxAdapter(
|
||||
child: DynamicPanel(
|
||||
item: _dynamicDetailController!.item,
|
||||
source: 'detail',
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
delegate: _MySliverPersistentHeaderDelegate(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 0.6,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.05),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
if (action != 'comment')
|
||||
SliverToBoxAdapter(
|
||||
child: DynamicPanel(
|
||||
item: _dynamicDetailController.item,
|
||||
source: 'detail',
|
||||
),
|
||||
),
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${_dynamicDetailController!.acount.value}',
|
||||
key: ValueKey<int>(
|
||||
_dynamicDetailController!.acount.value),
|
||||
SliverPersistentHeader(
|
||||
delegate: _MySliverPersistentHeaderDelegate(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 0.6,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('条回复'),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
_dynamicDetailController!.queryBySort(),
|
||||
icon: const Icon(Icons.sort, size: 16),
|
||||
label: Obx(() => Text(
|
||||
_dynamicDetailController!.sortTypeLabel.value,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => _dynamicDetailController!.replyList.isEmpty &&
|
||||
_dynamicDetailController!.isLoadingMore
|
||||
? SliverList(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index ==
|
||||
_dynamicDetailController!
|
||||
.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_dynamicDetailController!
|
||||
.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ReplyItem(
|
||||
replyItem: _dynamicDetailController!
|
||||
.replyList[index],
|
||||
showReplyRow: true,
|
||||
replyLevel: '1',
|
||||
replyReply: (replyItem) =>
|
||||
replyReply(replyItem),
|
||||
replyType: ReplyType.values[type],
|
||||
addReply: (replyItem) {
|
||||
_dynamicDetailController!
|
||||
.replyList[index].replies!
|
||||
.add(replyItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
childCount:
|
||||
_dynamicDetailController!.replyList.length +
|
||||
1,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${_dynamicDetailController.acount.value}',
|
||||
key: ValueKey<int>(
|
||||
_dynamicDetailController.acount.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('条回复'),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
_dynamicDetailController.queryBySort(),
|
||||
icon: const Icon(Icons.sort, size: 16),
|
||||
label: Obx(() => Text(
|
||||
_dynamicDetailController
|
||||
.sortTypeLabel.value,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => _dynamicDetailController.replyList.isEmpty &&
|
||||
_dynamicDetailController.isLoadingMore
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index ==
|
||||
_dynamicDetailController
|
||||
.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_dynamicDetailController
|
||||
.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ReplyItem(
|
||||
replyItem: _dynamicDetailController
|
||||
.replyList[index],
|
||||
showReplyRow: true,
|
||||
replyLevel: '1',
|
||||
replyReply: (replyItem) =>
|
||||
replyReply(replyItem),
|
||||
replyType: ReplyType.values[type],
|
||||
addReply: (replyItem) {
|
||||
_dynamicDetailController
|
||||
.replyList[index].replies!
|
||||
.add(replyItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
childCount: _dynamicDetailController
|
||||
.replyList.length +
|
||||
1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 14,
|
||||
right: 14,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 2),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: fabAnimationCtr,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return VideoReplyNewDialog(
|
||||
oid: _dynamicDetailController.oid ??
|
||||
IdUtils.bv2av(Get.parameters['bvid']!),
|
||||
root: 0,
|
||||
parent: 0,
|
||||
replyType: ReplyType.values[type],
|
||||
);
|
||||
},
|
||||
).then(
|
||||
(value) => {
|
||||
// 完成评论,数据添加
|
||||
if (value != null && value['data'] != null)
|
||||
{
|
||||
_dynamicDetailController.replyList
|
||||
.add(value['data']),
|
||||
_dynamicDetailController.acount.value++
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
tooltip: '评论动态',
|
||||
child: const Icon(Icons.reply),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -212,6 +212,9 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
future: _futureBuilderFutureUp,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
return Obx(() => UpPanel(_dynamicsController.upData.value));
|
||||
@ -232,6 +235,9 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
List<DynamicItemModel> list =
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
|
||||
/// TODO 点击跳转
|
||||
Widget addWidget(item, context, type, {floor = 1}) {
|
||||
@ -19,8 +22,27 @@ Widget addWidget(item, context, type, {floor = 1}) {
|
||||
: Theme.of(context).colorScheme.background;
|
||||
switch (type) {
|
||||
case 'ADDITIONAL_TYPE_UGC':
|
||||
// 转发的投稿
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
onTap: () async {
|
||||
String text = dynamicProperty[type].jumpUrl;
|
||||
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
|
||||
Iterable<Match> matches = bvRegex.allMatches(text);
|
||||
if (matches.isNotEmpty) {
|
||||
Match match = matches.first;
|
||||
String bvid = match.group(0)!;
|
||||
String cover = dynamicProperty[type].cover;
|
||||
try {
|
||||
int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
Get.toNamed('/video?bvid=$bvid&cid=$cid',
|
||||
arguments: {'pic': cover, 'heroTag': bvid});
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
} else {
|
||||
print("No match found.");
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
|
||||
@ -61,101 +83,111 @@ Widget addWidget(item, context, type, {floor = 1}) {
|
||||
);
|
||||
case 'ADDITIONAL_TYPE_RESERVE':
|
||||
return dynamicProperty[type].state != -1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, top: 10, right: 12, bottom: 10),
|
||||
color: bgColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
dynamicProperty[type].title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
? dynamicProperty[type].title != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, top: 10, right: 12, bottom: 10),
|
||||
color: bgColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
dynamicProperty[type].title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
children: [
|
||||
if (dynamicProperty[type].desc1 != null)
|
||||
TextSpan(
|
||||
text:
|
||||
dynamicProperty[type].desc1['text']),
|
||||
const TextSpan(text: ' '),
|
||||
if (dynamicProperty[type].desc2 != null)
|
||||
TextSpan(
|
||||
text:
|
||||
dynamicProperty[type].desc2['text']),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
children: [
|
||||
TextSpan(text: dynamicProperty[type].desc1['text']),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(text: dynamicProperty[type].desc2['text']),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
// TextButton(onPressed: () {}, child: Text('123'))
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
case 'ADDITIONAL_TYPE_GOODS':
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 75,
|
||||
height: 75,
|
||||
src: dynamicProperty[type].items.first.cover,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
dynamicProperty[type].items.first.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
dynamicProperty[type].items.first.brief,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
dynamicProperty[type].items.first.price,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
// TextButton(onPressed: () {}, child: Text('123'))
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
)
|
||||
: const SizedBox()
|
||||
: const SizedBox();
|
||||
case 'ADDITIONAL_TYPE_GOODS':
|
||||
// 商品
|
||||
return const SizedBox();
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(top: 6),
|
||||
// child: InkWell(
|
||||
// onTap: () {},
|
||||
// child: Container(
|
||||
// padding:
|
||||
// const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
|
||||
// decoration: BoxDecoration(
|
||||
// color: bgColor,
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// NetworkImgLayer(
|
||||
// width: 75,
|
||||
// height: 75,
|
||||
// src: dynamicProperty[type].items.first.cover,
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
// Expanded(
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(
|
||||
// dynamicProperty[type].items.first.name,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// Text(
|
||||
// dynamicProperty[type].items.first.brief,
|
||||
// maxLines: 1,
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).colorScheme.outline,
|
||||
// fontSize: Theme.of(context)
|
||||
// .textTheme
|
||||
// .labelMedium!
|
||||
// .fontSize,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 2),
|
||||
// Text(
|
||||
// dynamicProperty[type].items.first.price,
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),);
|
||||
case 'ADDITIONAL_TYPE_MATCH':
|
||||
return const SizedBox();
|
||||
case 'ADDITIONAL_TYPE_COMMON':
|
||||
|
||||
@ -1,65 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
Widget author(item, context) {
|
||||
String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
feedBack();
|
||||
Get.toNamed(
|
||||
'/member?mid=${item.modules.moduleAuthor.mid}',
|
||||
arguments: {
|
||||
'face': item.modules.moduleAuthor.face,
|
||||
'heroTag': heroTag
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
width: 40,
|
||||
height: 40,
|
||||
type: 'avatar',
|
||||
src: item.modules.moduleAuthor.face,
|
||||
class AuthorPanel extends StatelessWidget {
|
||||
final dynamic item;
|
||||
const AuthorPanel({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// 番剧
|
||||
if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
|
||||
return;
|
||||
}
|
||||
feedBack();
|
||||
Get.toNamed(
|
||||
'/member?mid=${item.modules.moduleAuthor.mid}',
|
||||
arguments: {
|
||||
'face': item.modules.moduleAuthor.face,
|
||||
'heroTag': heroTag
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
width: 40,
|
||||
height: 40,
|
||||
type: 'avatar',
|
||||
src: item.modules.moduleAuthor.face,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.modules.moduleAuthor.name,
|
||||
style: TextStyle(
|
||||
color: item.modules.moduleAuthor!.vip != null &&
|
||||
item.modules.moduleAuthor!.vip['status'] > 0
|
||||
? const Color.fromARGB(255, 251, 100, 163)
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.modules.moduleAuthor.name,
|
||||
style: TextStyle(
|
||||
color: item.modules.moduleAuthor!.vip != null &&
|
||||
item.modules.moduleAuthor!.vip['status'] > 0
|
||||
? const Color.fromARGB(255, 251, 100, 163)
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
),
|
||||
),
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(item.modules.moduleAuthor.pubTime),
|
||||
if (item.modules.moduleAuthor.pubTime != '' &&
|
||||
item.modules.moduleAuthor.pubAction != '')
|
||||
const Text(' '),
|
||||
Text(item.modules.moduleAuthor.pubAction),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
if (item.type == 'DYNAMIC_TYPE_AV')
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return MorePanel(item: item);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.more_vert_outlined, size: 18),
|
||||
),
|
||||
),
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MorePanel extends StatelessWidget {
|
||||
final dynamic item;
|
||||
const MorePanel({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||
// clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
height: 35,
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3))),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(item.modules.moduleAuthor.pubTime),
|
||||
if (item.modules.moduleAuthor.pubTime != '' &&
|
||||
item.modules.moduleAuthor.pubAction != '')
|
||||
const Text(' '),
|
||||
Text(item.modules.moduleAuthor.pubAction),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
try {
|
||||
String bvid = item.modules.moduleDynamic.major.archive.bvid;
|
||||
var res = await UserHttp.toViewLater(bvid: bvid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
Get.back();
|
||||
} catch (err) {
|
||||
SmartDialog.showToast('出错了:${err.toString()}');
|
||||
}
|
||||
},
|
||||
minLeadingWidth: 0,
|
||||
// dense: true,
|
||||
leading: const Icon(Icons.watch_later_outlined, size: 19),
|
||||
title: Text(
|
||||
'稍后再看',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
)
|
||||
),
|
||||
const Divider(thickness: 0.1, height: 1),
|
||||
ListTile(
|
||||
onTap: () => Get.back(),
|
||||
minLeadingWidth: 0,
|
||||
dense: true,
|
||||
title: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +1,183 @@
|
||||
// 内容
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
|
||||
import 'rich_node_panel.dart';
|
||||
|
||||
Widget content(item, context, source) {
|
||||
TextStyle authorStyle =
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary);
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (item.modules.moduleDynamic.topic != null) ...[
|
||||
GestureDetector(
|
||||
child: Text(
|
||||
'#${item.modules.moduleDynamic.topic.name}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
IgnorePointer(
|
||||
// 禁用SelectableRegion的触摸交互功能
|
||||
ignoring: source == 'detail' ? false : true,
|
||||
child: SelectableRegion(
|
||||
magnifierConfiguration: const TextMagnifierConfiguration(),
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Text.rich(
|
||||
richNode(item, context),
|
||||
maxLines: source == 'detail' ? 999 : 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// ignore: must_be_immutable
|
||||
class Content extends StatefulWidget {
|
||||
dynamic item;
|
||||
String? source;
|
||||
Content({
|
||||
super.key,
|
||||
this.item,
|
||||
this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Content> createState() => _ContentState();
|
||||
}
|
||||
|
||||
class _ContentState extends State<Content> {
|
||||
late bool hasPics;
|
||||
List<OpusPicsModel> pics = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hasPics = widget.item.modules.moduleDynamic.major != null &&
|
||||
widget.item.modules.moduleDynamic.major.opus != null &&
|
||||
widget.item.modules.moduleDynamic.major.opus.pics.isNotEmpty;
|
||||
if (hasPics) {
|
||||
pics = widget.item.modules.moduleDynamic.major.opus.pics;
|
||||
}
|
||||
}
|
||||
|
||||
InlineSpan picsNodes() {
|
||||
List<InlineSpan> spanChilds = [];
|
||||
int len = pics.length;
|
||||
List<String> picList = [];
|
||||
|
||||
if (len == 1) {
|
||||
OpusPicsModel pictureItem = pics.first;
|
||||
picList.add(pictureItem.url!);
|
||||
spanChilds.add(const TextSpan(text: '\n'));
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: 0, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: NetworkImgLayer(
|
||||
src: pictureItem.url,
|
||||
width: box.maxWidth / 2,
|
||||
height: box.maxWidth *
|
||||
0.5 *
|
||||
(pictureItem.height != null && pictureItem.width != null
|
||||
? pictureItem.height! / pictureItem.width!
|
||||
: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
if (len > 1) {
|
||||
List<Widget> list = [];
|
||||
for (var i = 0; i < len; i++) {
|
||||
picList.add(pics[i].url!);
|
||||
list.add(
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: i, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
src: pics[i].url,
|
||||
width: box.maxWidth,
|
||||
height: box.maxWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
double maxWidth = box.maxWidth;
|
||||
double crossCount = len < 3 ? 2 : 3;
|
||||
double height = maxWidth /
|
||||
crossCount *
|
||||
(len % crossCount == 0
|
||||
? len ~/ crossCount
|
||||
: len ~/ crossCount + 1) +
|
||||
6;
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
height: height,
|
||||
child: GridView.count(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: crossCount.toInt(),
|
||||
mainAxisSpacing: 4.0,
|
||||
crossAxisSpacing: 4.0,
|
||||
childAspectRatio: 1,
|
||||
children: list,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return TextSpan(
|
||||
children: spanChilds,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle authorStyle =
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.item.modules.moduleDynamic.topic != null) ...[
|
||||
GestureDetector(
|
||||
child: Text(
|
||||
'#${widget.item.modules.moduleDynamic.topic.name}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
IgnorePointer(
|
||||
// 禁用SelectableRegion的触摸交互功能
|
||||
ignoring: widget.source == 'detail' ? false : true,
|
||||
child: SelectableRegion(
|
||||
magnifierConfiguration: const TextMagnifierConfiguration(),
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Text.rich(
|
||||
/// fix 默认20px高度
|
||||
style: const TextStyle(height: 0),
|
||||
richNode(widget.item, context),
|
||||
maxLines: widget.source == 'detail' ? 999 : 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasPics) ...[
|
||||
Text.rich(picsNodes()),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,10 +39,11 @@ class DynamicPanel extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
|
||||
child: author(item, context),
|
||||
child: AuthorPanel(item: item),
|
||||
),
|
||||
if (item!.modules!.moduleDynamic!.desc != null)
|
||||
content(item, context, source),
|
||||
if (item!.modules!.moduleDynamic!.desc != null ||
|
||||
item!.modules!.moduleDynamic!.major != null)
|
||||
Content(item: item, source: source),
|
||||
forWard(item, context, _dynamicsController, source),
|
||||
const SizedBox(height: 2),
|
||||
if (source == null) ActionPanel(item: item),
|
||||
|
||||
@ -44,19 +44,21 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (item.modules.moduleDynamic.topic != null) ...[
|
||||
Padding(
|
||||
padding: floor == 2
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
'#${item.modules.moduleDynamic.topic.name}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
/// fix #话题跟content重复
|
||||
// if (item.modules.moduleDynamic.topic != null) ...[
|
||||
// Padding(
|
||||
// padding: floor == 2
|
||||
// ? EdgeInsets.zero
|
||||
// : const EdgeInsets.only(left: 12, right: 12),
|
||||
// child: GestureDetector(
|
||||
// child: Text(
|
||||
// '#${item.modules.moduleDynamic.topic.name}',
|
||||
// style: authorStyle,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
Text.rich(
|
||||
richNode(item, context),
|
||||
// 被转发状态(floor=2) 隐藏
|
||||
@ -71,6 +73,8 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: picWidget(item, context),
|
||||
),
|
||||
|
||||
/// 附加内容 商品信息、直播预约等等
|
||||
if (item.modules.moduleDynamic.additional != null)
|
||||
addWidget(
|
||||
item,
|
||||
@ -133,7 +137,12 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(item.modules.moduleDynamic.desc.text)
|
||||
Text.rich(
|
||||
richNode(item, context),
|
||||
// 被转发状态(floor=2) 隐藏
|
||||
maxLines: source == 'detail' && floor != 2 ? 999 : 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
)
|
||||
: item.modules.moduleDynamic.additional != null
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/badge.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
|
||||
Widget picWidget(item, context) {
|
||||
String type = item.modules.moduleDynamic.major.type;
|
||||
List pictures = [];
|
||||
if (type == 'MAJOR_TYPE_OPUS') {
|
||||
pictures = item.modules.moduleDynamic.major.opus.pics;
|
||||
/// fix 图片跟rich_node_panel重复
|
||||
// pictures = item.modules.moduleDynamic.major.opus.pics;
|
||||
return const SizedBox();
|
||||
}
|
||||
if (type == 'MAJOR_TYPE_DRAW') {
|
||||
pictures = item.modules.moduleDynamic.major.draw.items;
|
||||
}
|
||||
int len = pictures.length;
|
||||
List picList = [];
|
||||
List<String> picList = [];
|
||||
List<Widget> list = [];
|
||||
for (var i = 0; i < len; i++) {
|
||||
picList.add(pictures[i].src ?? pictures[i].url);
|
||||
@ -23,11 +25,14 @@ Widget picWidget(item, context) {
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': i, 'imgList': picList});
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: i, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
// child: Hero(
|
||||
// tag: pictures[i].src ?? pictures[i].url,
|
||||
child: NetworkImgLayer(
|
||||
src: pictures[i].src ?? pictures[i].url,
|
||||
width: box.maxWidth,
|
||||
|
||||
@ -1,175 +1,324 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
|
||||
// 富文本
|
||||
InlineSpan richNode(item, context) {
|
||||
TextStyle authorStyle =
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary);
|
||||
List<InlineSpan> spanChilds = [];
|
||||
for (var i in item.modules.moduleDynamic.desc.richTextNodes) {
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
|
||||
spanChilds.add(
|
||||
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
|
||||
final spacer = _VerticalSpaceSpan(0.0);
|
||||
try {
|
||||
TextStyle authorStyle =
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary);
|
||||
List<InlineSpan> spanChilds = [];
|
||||
String contentType = 'desc';
|
||||
|
||||
dynamic richTextNodes;
|
||||
if (item.modules.moduleDynamic.desc != null) {
|
||||
richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
|
||||
} else if (item.modules.moduleDynamic.major != null) {
|
||||
contentType = 'major';
|
||||
// 动态页面 richTextNodes 层级可能与主页动态层级不同
|
||||
richTextNodes =
|
||||
item.modules.moduleDynamic.major.opus.summary.richTextNodes;
|
||||
}
|
||||
// @用户
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.toNamed('/member?mid=${i.rid}',
|
||||
arguments: {'face': null}),
|
||||
if (richTextNodes == null || richTextNodes.isEmpty) {
|
||||
return spacer;
|
||||
} else {
|
||||
for (var i in richTextNodes) {
|
||||
/// fix 渲染专栏时内容会重复
|
||||
// if (item.modules.moduleDynamic.major.opus.title == null &&
|
||||
// i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
|
||||
spanChilds.add(
|
||||
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
|
||||
}
|
||||
// @用户
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.toNamed('/member?mid=${i.rid}',
|
||||
arguments: {'face': null}),
|
||||
child: Text(
|
||||
' ${i.text}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 话题
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
' ${i.text}',
|
||||
'${i.origText}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 话题
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.origText}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 网页链接
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.link,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
i.text,
|
||||
style: authorStyle,
|
||||
);
|
||||
}
|
||||
// 网页链接
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.link,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 投票
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
String dynamicId = item.basic['comment_id_str'];
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
|
||||
'type': 'vote',
|
||||
'pageTitle': '投票'
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': i.origText,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'投票:${i.text}',
|
||||
style: authorStyle,
|
||||
child: Text(
|
||||
i.text,
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 表情
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: i.emoji.iconUrl,
|
||||
type: 'emote',
|
||||
width: i.emoji.size * 20,
|
||||
height: i.emoji.size * 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 抽奖
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.redeem_rounded,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.origText} ',
|
||||
style: authorStyle,
|
||||
);
|
||||
}
|
||||
// 投票
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
try {
|
||||
String dynamicId = item.basic['comment_id_str'];
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
|
||||
'type': 'vote',
|
||||
'pageTitle': '投票'
|
||||
},
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
child: Text(
|
||||
'投票:${i.text}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
// 表情
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: i.emoji.iconUrl,
|
||||
type: 'emote',
|
||||
width: i.emoji.size * 20,
|
||||
height: i.emoji.size * 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 抽奖
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.redeem_rounded,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.origText} ',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// TODO 商品
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.text} ',
|
||||
style: authorStyle,
|
||||
/// TODO 商品
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.text} ',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// if (contentType == 'major' &&
|
||||
// item.modules.moduleDynamic.major.opus.pics.isNotEmpty) {
|
||||
// // 图片可能跟其他widget重复渲染
|
||||
// List<OpusPicsModel> pics = item.modules.moduleDynamic.major.opus.pics;
|
||||
// int len = pics.length;
|
||||
// List<String> picList = [];
|
||||
|
||||
// if (len == 1) {
|
||||
// OpusPicsModel pictureItem = pics.first;
|
||||
// picList.add(pictureItem.url!);
|
||||
// spanChilds.add(const TextSpan(text: '\n'));
|
||||
// spanChilds.add(
|
||||
// WidgetSpan(
|
||||
// child: LayoutBuilder(
|
||||
// builder: (context, BoxConstraints box) {
|
||||
// return GestureDetector(
|
||||
// onTap: () {
|
||||
// showDialog(
|
||||
// useSafeArea: false,
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return ImagePreview(initialPage: 0, imgList: picList);
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(top: 4),
|
||||
// child: NetworkImgLayer(
|
||||
// src: pictureItem.url,
|
||||
// width: box.maxWidth / 2,
|
||||
// height: box.maxWidth *
|
||||
// 0.5 *
|
||||
// (pictureItem.height != null &&
|
||||
// pictureItem.width != null
|
||||
// ? pictureItem.height! / pictureItem.width!
|
||||
// : 1),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// if (len > 1) {
|
||||
// List<Widget> list = [];
|
||||
// for (var i = 0; i < len; i++) {
|
||||
// picList.add(pics[i].url!);
|
||||
// list.add(
|
||||
// LayoutBuilder(
|
||||
// builder: (context, BoxConstraints box) {
|
||||
// return GestureDetector(
|
||||
// onTap: () {
|
||||
// showDialog(
|
||||
// useSafeArea: false,
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return ImagePreview(initialPage: i, imgList: picList);
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// child: NetworkImgLayer(
|
||||
// src: pics[i].url,
|
||||
// width: box.maxWidth,
|
||||
// height: box.maxWidth,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// spanChilds.add(
|
||||
// WidgetSpan(
|
||||
// child: LayoutBuilder(
|
||||
// builder: (context, BoxConstraints box) {
|
||||
// double maxWidth = box.maxWidth;
|
||||
// double crossCount = len < 3 ? 2 : 3;
|
||||
// double height = maxWidth /
|
||||
// crossCount *
|
||||
// (len % crossCount == 0
|
||||
// ? len ~/ crossCount
|
||||
// : len ~/ crossCount + 1) +
|
||||
// 6;
|
||||
// return Container(
|
||||
// padding: const EdgeInsets.only(top: 6),
|
||||
// height: height,
|
||||
// child: GridView.count(
|
||||
// padding: EdgeInsets.zero,
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// crossAxisCount: crossCount.toInt(),
|
||||
// mainAxisSpacing: 4.0,
|
||||
// crossAxisSpacing: 4.0,
|
||||
// childAspectRatio: 1,
|
||||
// children: list,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// spanChilds.add(
|
||||
// WidgetSpan(
|
||||
// child: NetworkImgLayer(
|
||||
// src: pics.first.url,
|
||||
// type: 'emote',
|
||||
// width: 100,
|
||||
// height: 200,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
return TextSpan(
|
||||
children: spanChilds,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
print('❌rich_node_panel err: $err');
|
||||
return spacer;
|
||||
}
|
||||
return TextSpan(
|
||||
children: spanChilds,
|
||||
);
|
||||
}
|
||||
|
||||
class _VerticalSpaceSpan extends WidgetSpan {
|
||||
_VerticalSpaceSpan(double height)
|
||||
: super(child: SizedBox(height: height, width: double.infinity));
|
||||
}
|
||||
|
||||
@ -91,7 +91,10 @@ class _UpPanelState extends State<UpPanel> {
|
||||
),
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () => {feedBack(), Get.toNamed('/follow')},
|
||||
onTap: () => {
|
||||
feedBack(),
|
||||
Get.toNamed('/follow?mid=${userInfo.mid}')
|
||||
},
|
||||
child: Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||
|
||||
@ -57,20 +57,21 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
// const SizedBox(height: 4),
|
||||
if (item.modules.moduleDynamic.topic != null) ...[
|
||||
Padding(
|
||||
padding: floor == 2
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
'#${item.modules.moduleDynamic.topic.name}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
/// fix #话题跟content重复
|
||||
// if (item.modules.moduleDynamic.topic != null) ...[
|
||||
// Padding(
|
||||
// padding: floor == 2
|
||||
// ? EdgeInsets.zero
|
||||
// : const EdgeInsets.only(left: 12, right: 12),
|
||||
// child: GestureDetector(
|
||||
// child: Text(
|
||||
// '#${item.modules.moduleDynamic.topic.name}',
|
||||
// style: authorStyle,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 6),
|
||||
// ],
|
||||
if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[
|
||||
Text.rich(richNode(item, context)),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
@ -16,13 +16,16 @@ class FansPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FansPageState extends State<FansPage> {
|
||||
final FansController _fansController = Get.put(FansController());
|
||||
late String mid;
|
||||
late FansController _fansController;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Future? _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mid = Get.parameters['mid']!;
|
||||
_fansController = Get.put(FansController(), tag: mid);
|
||||
_futureBuilderFuture = _fansController.queryFans('init');
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
|
||||
@ -44,6 +44,14 @@ class _FavPageState extends State<FavPage> {
|
||||
'我的收藏',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed(
|
||||
'/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
|
||||
@ -14,7 +14,7 @@ class FavDetailController extends GetxController {
|
||||
int currentPage = 1;
|
||||
bool isLoadingMore = false;
|
||||
RxMap favInfo = {}.obs;
|
||||
RxList<FavDetailItemData> favList = [FavDetailItemData()].obs;
|
||||
RxList favList = [].obs;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
int mediaCount = 0;
|
||||
|
||||
@ -61,15 +61,13 @@ class FavDetailController extends GetxController {
|
||||
aid: id, addIds: '', delIds: mediaId.toString());
|
||||
if (result['status']) {
|
||||
if (result['data']['prompt']) {
|
||||
List<FavDetailItemData> dataList = favDetailData.value.medias!;
|
||||
List dataList = favList;
|
||||
for (var i in dataList) {
|
||||
if (i.id == id) {
|
||||
dataList.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
favDetailData.value.medias = dataList;
|
||||
favDetailData.refresh();
|
||||
SmartDialog.showToast('取消收藏');
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,13 +92,18 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(Icons.more_vert),
|
||||
// ),
|
||||
// const SizedBox(width: 4)
|
||||
// ],
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed(
|
||||
'/favSearch?searchType=0&mediaId=${Get.parameters['mediaId']!}'),
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
),
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(Icons.more_vert),
|
||||
// ),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
@ -168,7 +173,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
|
||||
child: Obx(
|
||||
() => Text(
|
||||
'共${_favDetailController.favInfo['media_count'] ?? '-'}条视频',
|
||||
'共${_favDetailController.favList.length}条视频',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
@ -187,14 +192,20 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
if (_favDetailController.item!.mediaCount == 0) {
|
||||
return const NoData();
|
||||
} else {
|
||||
List favList = _favDetailController.favList;
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return FavVideoCardH(
|
||||
videoItem: _favDetailController.favList[index],
|
||||
);
|
||||
}, childCount: _favDetailController.favList.length),
|
||||
),
|
||||
() => favList.isEmpty
|
||||
? const SliverToBoxAdapter(child: SizedBox())
|
||||
: SliverList(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((context, index) {
|
||||
return FavVideoCardH(
|
||||
videoItem: favList[index],
|
||||
callFn: () => _favDetailController
|
||||
.onCancelFav(favList[index].id),
|
||||
);
|
||||
}, childCount: favList.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -10,134 +10,109 @@ import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
|
||||
import '../controller.dart';
|
||||
|
||||
// 收藏视频卡片 - 水平布局
|
||||
class FavVideoCardH extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
final FavDetailController _favDetailController =
|
||||
Get.put(FavDetailController());
|
||||
final Function? callFn;
|
||||
|
||||
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key);
|
||||
const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int id = videoItem.id;
|
||||
String bvid = videoItem.bvid ?? IdUtils.av2bv(id);
|
||||
String heroTag = Utils.makeHeroTag(id);
|
||||
return Dismissible(
|
||||
movementDuration: const Duration(milliseconds: 300),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.clear_all_rounded),
|
||||
SizedBox(width: 6),
|
||||
Text('取消收藏')
|
||||
],
|
||||
)),
|
||||
direction: DismissDirection.endToStart,
|
||||
key: ValueKey<int>(videoItem.id),
|
||||
onDismissed: (DismissDirection direction) {
|
||||
_favDetailController.onCancelFav(videoItem.id);
|
||||
// widget.onDeleteNotice();
|
||||
},
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
// int? seasonId;
|
||||
String? epId;
|
||||
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
|
||||
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
// seasonId = videoItem.ogv['season_id'];
|
||||
epId = videoItem.epId;
|
||||
} else if (videoItem.page == 0 || videoItem.page > 1) {
|
||||
var result = await VideoHttp.videoIntro(bvid: bvid);
|
||||
if (result['status']) {
|
||||
epId = result['data'].epId;
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
// int? seasonId;
|
||||
String? epId;
|
||||
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
|
||||
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
// seasonId = videoItem.ogv['season_id'];
|
||||
epId = videoItem.epId;
|
||||
} else if (videoItem.page == 0 || videoItem.page > 1) {
|
||||
var result = await VideoHttp.videoIntro(bvid: bvid);
|
||||
if (result['status']) {
|
||||
epId = result['data'].epId;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> parameters = {
|
||||
'bvid': bvid,
|
||||
'cid': videoItem.cid.toString(),
|
||||
'epId': epId ?? '',
|
||||
};
|
||||
// if (seasonId != null) {
|
||||
// parameters['seasonId'] = seasonId.toString();
|
||||
// }
|
||||
Get.toNamed('/video', parameters: parameters, arguments: {
|
||||
'videoItem': videoItem,
|
||||
'heroTag': heroTag,
|
||||
'videoType':
|
||||
epId != null ? SearchType.media_bangumi : SearchType.video,
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
Map<String, String> parameters = {
|
||||
'bvid': bvid,
|
||||
'cid': videoItem.cid.toString(),
|
||||
'epId': epId ?? '',
|
||||
};
|
||||
// if (seasonId != null) {
|
||||
// parameters['seasonId'] = seasonId.toString();
|
||||
// }
|
||||
Get.toNamed('/video', parameters: parameters, arguments: {
|
||||
'videoItem': videoItem,
|
||||
'heroTag': heroTag,
|
||||
'videoType':
|
||||
epId != null ? SearchType.media_bangumi : SearchType.video,
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Colors.black54.withOpacity(0.4)),
|
||||
child: Text(
|
||||
Utils.timeFormat(videoItem.duration!),
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
color:
|
||||
Colors.black54.withOpacity(0.4)),
|
||||
child: Text(
|
||||
Utils.timeFormat(videoItem.duration!),
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
VideoContent(videoItem: videoItem, callFn: callFn)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -145,7 +120,8 @@ class FavVideoCardH extends StatelessWidget {
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const VideoContent({super.key, required this.videoItem});
|
||||
final Function? callFn;
|
||||
const VideoContent({super.key, required this.videoItem, this.callFn});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -173,7 +149,6 @@ class VideoContent extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
@ -181,7 +156,51 @@ class VideoContent extends StatelessWidget {
|
||||
view: videoItem.cntInfo['play'],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
|
||||
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('要取消收藏吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await callFn!();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定取消'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
75
lib/pages/fav_search/controller.dart
Normal file
75
lib/pages/fav_search/controller.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/user/fav_detail.dart';
|
||||
|
||||
class FavSearchController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Rx<TextEditingController> controller = TextEditingController().obs;
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs; // 搜索词
|
||||
String hintText = '请输入已收藏视频名称'; // 默认
|
||||
RxBool loadingStatus = false.obs; // 加载状态
|
||||
RxString loadingText = '加载中...'.obs; // 加载提示
|
||||
bool hasMore = false;
|
||||
late int searchType;
|
||||
late int mediaId;
|
||||
|
||||
int currentPage = 1; // 当前页
|
||||
int count = 0; // 总数
|
||||
RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
searchType = int.parse(Get.parameters['searchType']!);
|
||||
mediaId = int.parse(Get.parameters['mediaId']!);
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
void onClear() {
|
||||
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
|
||||
controller.value.clear();
|
||||
searchKeyWord.value = '';
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
void onChange(value) {
|
||||
searchKeyWord.value = value;
|
||||
}
|
||||
|
||||
// 提交搜索内容
|
||||
void submit() {
|
||||
loadingStatus.value = true;
|
||||
currentPage = 1;
|
||||
searchFav();
|
||||
}
|
||||
|
||||
// 搜索收藏夹视频
|
||||
Future searchFav({type = 'init'}) async {
|
||||
var res = await await UserHttp.userFavFolderDetail(
|
||||
pn: currentPage,
|
||||
ps: 20,
|
||||
mediaId: mediaId,
|
||||
keyword: searchKeyWord.value,
|
||||
type: searchType,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (currentPage == 1 && type == 'init') {
|
||||
favList.value = res['data'].medias;
|
||||
} else if (type == 'onLoad') {
|
||||
favList.addAll(res['data'].medias);
|
||||
}
|
||||
hasMore = res['data'].hasMore;
|
||||
}
|
||||
currentPage += 1;
|
||||
loadingStatus.value = false;
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
if (!hasMore) return;
|
||||
searchFav(type: 'onLoad');
|
||||
}
|
||||
}
|
||||
4
lib/pages/fav_search/index.dart
Normal file
4
lib/pages/fav_search/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library fav_search;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
116
lib/pages/fav_search/view.dart
Normal file
116
lib/pages/fav_search/view.dart
Normal file
@ -0,0 +1,116 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/pages/favDetail/widget/fav_video_card.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class FavSearchPage extends StatefulWidget {
|
||||
final int? sourceType;
|
||||
final int? mediaId;
|
||||
const FavSearchPage({super.key, this.sourceType, this.mediaId});
|
||||
|
||||
@override
|
||||
State<FavSearchPage> createState() => _FavSearchPageState();
|
||||
}
|
||||
|
||||
class _FavSearchPageState extends State<FavSearchPage> {
|
||||
final FavSearchController _favSearchCtr = Get.put(FavSearchController());
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
scrollController = _favSearchCtr.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('fav', const Duration(seconds: 1), () {
|
||||
_favSearchCtr.onLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _favSearchCtr.submit(),
|
||||
icon: const Icon(Icons.search_outlined, size: 22)),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: Obx(
|
||||
() => TextField(
|
||||
autofocus: true,
|
||||
focusNode: _favSearchCtr.searchFocusNode,
|
||||
controller: _favSearchCtr.controller.value,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (value) => _favSearchCtr.onChange(value),
|
||||
decoration: InputDecoration(
|
||||
hintText: _favSearchCtr.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 22,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
onPressed: () => _favSearchCtr.onClear(),
|
||||
),
|
||||
),
|
||||
onSubmitted: (String value) => _favSearchCtr.submit(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => _favSearchCtr.loadingStatus.value && _favSearchCtr.favList.isEmpty
|
||||
? ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
)
|
||||
: _favSearchCtr.favList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: _favSearchCtr.favList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _favSearchCtr.favList.length) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).padding.bottom + 60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom),
|
||||
);
|
||||
} else {
|
||||
return FavVideoCardH(
|
||||
videoItem: _favSearchCtr.favList[index],
|
||||
callFn: () => null,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
NoData(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/follow.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
import 'package:pilipala/models/member/tags.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class FollowController extends GetxController {
|
||||
/// 查看自己的关注时,可以查看分类
|
||||
/// 查看其他人的关注时,只可以看全部
|
||||
class FollowController extends GetxController with GetTickerProviderStateMixin {
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
int pn = 1;
|
||||
int ps = 20;
|
||||
int total = 0;
|
||||
RxList<FollowItemModel> followList = [FollowItemModel()].obs;
|
||||
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
|
||||
late int mid;
|
||||
late String name;
|
||||
var userInfo;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
RxBool isOwner = false.obs;
|
||||
late List<MemberTagItemModel> followTags;
|
||||
late TabController tabController;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -23,6 +31,7 @@ class FollowController extends GetxController {
|
||||
mid = Get.parameters['mid'] != null
|
||||
? int.parse(Get.parameters['mid']!)
|
||||
: userInfo.mid;
|
||||
isOwner.value = mid == userInfo.mid;
|
||||
name = Get.parameters['name'] ?? userInfo.uname;
|
||||
}
|
||||
|
||||
@ -56,4 +65,20 @@ class FollowController extends GetxController {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// 当查看当前用户的关注时,请求关注分组
|
||||
Future followUpTags() async {
|
||||
if (userInfo != null && mid == userInfo.mid) {
|
||||
var res = await MemberHttp.followUpTags();
|
||||
if (res['status']) {
|
||||
followTags = res['data'];
|
||||
tabController = TabController(
|
||||
initialIndex: 0,
|
||||
length: res['data'].length,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'widgets/follow_item.dart';
|
||||
import 'widgets/follow_list.dart';
|
||||
import 'widgets/owner_follow_list.dart';
|
||||
|
||||
class FollowPage extends StatefulWidget {
|
||||
const FollowPage({super.key});
|
||||
@ -16,30 +12,15 @@ class FollowPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FollowPageState extends State<FollowPage> {
|
||||
final FollowController _followController = Get.put(FollowController());
|
||||
late String mid;
|
||||
late FollowController _followController;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Future? _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _followController.queryFollowings('init');
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
_followController.queryFollowings('onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
mid = Get.parameters['mid']!;
|
||||
_followController = Get.put(FollowController(), tag: mid);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -51,73 +32,57 @@ class _FollowPageState extends State<FollowPage> {
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'${_followController.name}的关注',
|
||||
_followController.isOwner.value
|
||||
? '我的关注'
|
||||
: '${_followController.name}的关注',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
await _followController.queryFollowings('init'),
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
List<FollowItemModel> list = _followController.followList;
|
||||
return Obx(
|
||||
() => list.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: list.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == list.length) {
|
||||
return Container(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_followController.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return followItem(item: list[index]);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(
|
||||
slivers: [NoData()],
|
||||
body: Obx(
|
||||
() => !_followController.isOwner.value
|
||||
? FollowList(ctr: _followController)
|
||||
: FutureBuilder(
|
||||
future: _followController.followUpTags(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
return Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _followController.tabController,
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
for (var i in data['data']) ...[
|
||||
Tab(text: i.name),
|
||||
]
|
||||
]),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _followController.tabController,
|
||||
children: [
|
||||
for (var i = 0;
|
||||
i < _followController.tabController.length;
|
||||
i++) ...[
|
||||
OwnerFollowList(
|
||||
ctr: _followController,
|
||||
tagItem: _followController.followTags[i],
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => _followController.queryFollowings('init'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
)),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,38 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
import 'package:pilipala/pages/follow/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/widgets/group_panel.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
Widget followItem({item}) {
|
||||
String heroTag = Utils.makeHeroTag(item!.mid);
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
feedBack();
|
||||
Get.toNamed('/member?mid=${item.mid}',
|
||||
arguments: {'face': item.face, 'heroTag': heroTag});
|
||||
},
|
||||
leading: Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
width: 45,
|
||||
height: 45,
|
||||
type: 'avatar',
|
||||
src: item.face,
|
||||
class FollowItem extends StatelessWidget {
|
||||
final FollowItemModel item;
|
||||
final FollowController? ctr;
|
||||
const FollowItem({super.key, required this.item, this.ctr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String heroTag = Utils.makeHeroTag(item.mid);
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
feedBack();
|
||||
Get.toNamed('/member?mid=${item.mid}',
|
||||
arguments: {'face': item.face, 'heroTag': heroTag});
|
||||
},
|
||||
leading: Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
width: 45,
|
||||
height: 45,
|
||||
type: 'avatar',
|
||||
src: item.face,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
item.uname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
item.sign,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
dense: true,
|
||||
trailing: const SizedBox(width: 6),
|
||||
);
|
||||
title: Text(
|
||||
item.uname!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
item.sign!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
dense: true,
|
||||
trailing: ctr!.isOwner.value
|
||||
? SizedBox(
|
||||
height: 34,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
await Get.bottomSheet(
|
||||
GroupPanel(mid: item.mid!),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色
|
||||
),
|
||||
child: const Text(
|
||||
'已关注',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
114
lib/pages/follow/widgets/follow_list.dart
Normal file
114
lib/pages/follow/widgets/follow_list.dart
Normal file
@ -0,0 +1,114 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
import 'package:pilipala/pages/follow/index.dart';
|
||||
|
||||
import 'follow_item.dart';
|
||||
|
||||
class FollowList extends StatefulWidget {
|
||||
final FollowController ctr;
|
||||
const FollowList({
|
||||
super.key,
|
||||
required this.ctr,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FollowList> createState() => _FollowListState();
|
||||
}
|
||||
|
||||
class _FollowListState extends State<FollowList> {
|
||||
late Future _futureBuilderFuture;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = widget.ctr.queryFollowings('init');
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
widget.ctr.queryFollowings('onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await widget.ctr.queryFollowings('init'),
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
List<FollowItemModel> list = widget.ctr.followList;
|
||||
return Obx(
|
||||
() => list.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: list.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == list.length) {
|
||||
return Container(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom + 60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
widget.ctr.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return FollowItem(
|
||||
item: list[index],
|
||||
ctr: widget.ctr,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(slivers: [NoData()]),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => widget.ctr.queryFollowings('init'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
134
lib/pages/follow/widgets/owner_follow_list.dart
Normal file
134
lib/pages/follow/widgets/owner_follow_list.dart
Normal file
@ -0,0 +1,134 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
import 'package:pilipala/models/member/tags.dart';
|
||||
import 'package:pilipala/pages/follow/index.dart';
|
||||
import 'follow_item.dart';
|
||||
|
||||
class OwnerFollowList extends StatefulWidget {
|
||||
final FollowController ctr;
|
||||
final MemberTagItemModel? tagItem;
|
||||
const OwnerFollowList({super.key, required this.ctr, this.tagItem});
|
||||
|
||||
@override
|
||||
State<OwnerFollowList> createState() => _OwnerFollowListState();
|
||||
}
|
||||
|
||||
class _OwnerFollowListState extends State<OwnerFollowList>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late int mid;
|
||||
late Future _futureBuilderFuture;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
int pn = 1;
|
||||
int ps = 20;
|
||||
late MemberTagItemModel tagItem;
|
||||
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mid = widget.ctr.mid;
|
||||
tagItem = widget.tagItem!;
|
||||
_futureBuilderFuture = followUpGroup('init');
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
followUpGroup('onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 获取分组下up
|
||||
Future followUpGroup(type) async {
|
||||
if (type == 'init') {
|
||||
pn = 1;
|
||||
}
|
||||
var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps);
|
||||
if (res['status']) {
|
||||
if (res['data'].isNotEmpty) {
|
||||
if (type == 'init') {
|
||||
followList.value = res['data'];
|
||||
} else {
|
||||
followList.addAll(res['data']);
|
||||
}
|
||||
pn += 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await followUpGroup('init'),
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => followList.isNotEmpty
|
||||
? ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
itemCount: followList.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == followList.length) {
|
||||
return Container(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom + 60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom),
|
||||
);
|
||||
} else {
|
||||
return FollowItem(
|
||||
item: followList[index],
|
||||
ctr: widget.ctr,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(slivers: [NoData()]),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => widget.ctr.queryFollowings('init'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HistoryController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
RxList<HisListItem> historyList = [HisListItem()].obs;
|
||||
RxList<HisListItem> historyList = <HisListItem>[].obs;
|
||||
RxBool isLoadingMore = false.obs;
|
||||
RxBool pauseStatus = false.obs;
|
||||
Box localCache = GStrorage.localCache;
|
||||
RxBool isLoading = false.obs;
|
||||
RxBool enableMultiple = false.obs;
|
||||
RxInt checkedCount = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -121,4 +123,80 @@ class HistoryController extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 删除某条历史记录
|
||||
Future delHistory(kid, business) async {
|
||||
String resKid = 'archive_$kid';
|
||||
if (business == 'live') {
|
||||
resKid = 'live_$kid';
|
||||
} else if (business.contains('article')) {
|
||||
resKid = 'article_$kid';
|
||||
}
|
||||
|
||||
var res = await UserHttp.delHistory(resKid);
|
||||
if (res['status']) {
|
||||
historyList.removeWhere((e) => e.kid == kid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除已看历史记录
|
||||
Future onDelHistory() async {
|
||||
/// TODO 优化
|
||||
List<HisListItem> result =
|
||||
historyList.where((e) => e.progress == -1).toList();
|
||||
for (HisListItem i in result) {
|
||||
String resKid = 'archive_${i.kid}';
|
||||
await UserHttp.delHistory(resKid);
|
||||
historyList.removeWhere((e) => e.kid == i.kid);
|
||||
}
|
||||
SmartDialog.showToast('操作完成');
|
||||
}
|
||||
|
||||
// 删除选中的记录
|
||||
Future onDelCheckedHistory() async {
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('确认删除所选历史记录吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
/// TODO 优化
|
||||
await SmartDialog.dismiss();
|
||||
SmartDialog.showLoading(msg: '请求中');
|
||||
List<HisListItem> result =
|
||||
historyList.where((e) => e.checked!).toList();
|
||||
for (HisListItem i in result) {
|
||||
String str = 'archive';
|
||||
try {
|
||||
str = i.history!.business!;
|
||||
} catch (_) {}
|
||||
String resKid = '${str}_${i.kid}';
|
||||
await UserHttp.delHistory(resKid);
|
||||
historyList.removeWhere((e) => e.kid == i.kid);
|
||||
}
|
||||
checkedCount.value = 0;
|
||||
SmartDialog.dismiss();
|
||||
enableMultiple.value = false;
|
||||
},
|
||||
child: const Text('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,23 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
}
|
||||
},
|
||||
);
|
||||
_historyController.enableMultiple.listen((p0) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
// 选中
|
||||
onChoose(index) {
|
||||
_historyController.historyList[index].checked =
|
||||
!_historyController.historyList[index].checked!;
|
||||
_historyController.checkedCount.value =
|
||||
_historyController.historyList.where((item) => item.checked!).length;
|
||||
_historyController.historyList.refresh();
|
||||
}
|
||||
|
||||
// 更新多选状态
|
||||
onUpdateMultiple() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -48,44 +65,112 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'观看记录',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (String type) {
|
||||
// 处理菜单项选择的逻辑
|
||||
switch (type) {
|
||||
case 'pause':
|
||||
_historyController.onPauseHistory();
|
||||
break;
|
||||
case 'clear':
|
||||
_historyController.onClearHistory();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'pause',
|
||||
child: Obx(
|
||||
() => Text(!_historyController.pauseStatus.value
|
||||
? '暂停观看记录'
|
||||
: '恢复观看记录'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'clear',
|
||||
child: Text('清空观看记录'),
|
||||
),
|
||||
],
|
||||
appBar: AppBarWidget(
|
||||
visible: _historyController.enableMultiple.value,
|
||||
child1: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.arrow_back_outlined),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
title: Text(
|
||||
'观看记录',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed('/historySearch'),
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (String type) {
|
||||
// 处理菜单项选择的逻辑
|
||||
switch (type) {
|
||||
case 'pause':
|
||||
_historyController.onPauseHistory();
|
||||
break;
|
||||
case 'clear':
|
||||
_historyController.onClearHistory();
|
||||
break;
|
||||
case 'del':
|
||||
_historyController.onDelHistory();
|
||||
break;
|
||||
case 'multiple':
|
||||
_historyController.enableMultiple.value = true;
|
||||
setState(() {});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'pause',
|
||||
child: Obx(
|
||||
() => Text(!_historyController.pauseStatus.value
|
||||
? '暂停观看记录'
|
||||
: '恢复观看记录'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'clear',
|
||||
child: Text('清空观看记录'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'del',
|
||||
child: Text('删除已看记录'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'multiple',
|
||||
child: Text('多选删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
child2: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
_historyController.enableMultiple.value = false;
|
||||
for (var item in _historyController.historyList) {
|
||||
item.checked = false;
|
||||
}
|
||||
_historyController.checkedCount.value = 0;
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
),
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'已选择${_historyController.checkedCount.value}项',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
for (var item in _historyController.historyList) {
|
||||
item.checked = true;
|
||||
}
|
||||
_historyController.checkedCount.value =
|
||||
_historyController.historyList.length;
|
||||
_historyController.historyList.refresh();
|
||||
},
|
||||
child: const Text('全选'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _historyController.onDelCheckedHistory(),
|
||||
child: Text(
|
||||
'删除',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
@ -99,6 +184,9 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
@ -109,6 +197,9 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
return HistoryItem(
|
||||
videoItem:
|
||||
_historyController.historyList[index],
|
||||
ctr: _historyController,
|
||||
onChoose: () => onChoose(index),
|
||||
onUpdateMultiple: () => onUpdateMultiple(),
|
||||
);
|
||||
},
|
||||
childCount:
|
||||
@ -144,6 +235,36 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// bottomNavigationBar: BottomAppBar(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
|
||||
const AppBarWidget({
|
||||
required this.child1,
|
||||
required this.child2,
|
||||
required this.visible,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final PreferredSizeWidget child1;
|
||||
final PreferredSizeWidget child2;
|
||||
final bool visible;
|
||||
@override
|
||||
Size get preferredSize => child1.preferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: !visible ? child1 : child2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,12 +11,24 @@ import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/common/business_type.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/models/live/item.dart';
|
||||
import 'package:pilipala/pages/history/index.dart';
|
||||
import 'package:pilipala/pages/history_search/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class HistoryItem extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const HistoryItem({super.key, required this.videoItem});
|
||||
final dynamic ctr;
|
||||
final Function? onChoose;
|
||||
final Function? onUpdateMultiple;
|
||||
const HistoryItem({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.ctr,
|
||||
this.onChoose,
|
||||
this.onUpdateMultiple,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -25,6 +37,11 @@ class HistoryItem extends StatelessWidget {
|
||||
String heroTag = Utils.makeHeroTag(aid);
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (ctr!.enableMultiple.value) {
|
||||
feedBack();
|
||||
onChoose!();
|
||||
return;
|
||||
}
|
||||
if (videoItem.history.business.contains('article')) {
|
||||
int cid = videoItem.history.cid ??
|
||||
// videoItem.history.oid ??
|
||||
@ -115,6 +132,17 @@ class HistoryItem extends StatelessWidget {
|
||||
arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (ctr is HistorySearchController) {
|
||||
return;
|
||||
}
|
||||
if (!ctr!.enableMultiple.value) {
|
||||
feedBack();
|
||||
ctr!.enableMultiple.value = true;
|
||||
onChoose!();
|
||||
onUpdateMultiple!();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
@ -130,53 +158,110 @@ class HistoryItem extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: (videoItem.cover != ''
|
||||
? videoItem.cover
|
||||
: videoItem.covers.first),
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: (videoItem.cover != ''
|
||||
? videoItem.cover
|
||||
: videoItem.covers.first),
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
if (!BusinessType
|
||||
.hiddenDurationType.hiddenDurationType
|
||||
.contains(videoItem.history.business))
|
||||
PBadge(
|
||||
text: videoItem.progress == -1
|
||||
? '已看完'
|
||||
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
// 右上角
|
||||
if (BusinessType.showBadge.showBadge
|
||||
.contains(
|
||||
videoItem.history.business) ||
|
||||
videoItem.history.business ==
|
||||
BusinessType.live.type)
|
||||
PBadge(
|
||||
text: videoItem.badge,
|
||||
top: 6.0,
|
||||
right: 6.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Positioned.fill(
|
||||
child: AnimatedOpacity(
|
||||
opacity: ctr!.enableMultiple.value ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.black.withOpacity(
|
||||
ctr!.enableMultiple.value &&
|
||||
videoItem.checked
|
||||
? 0.6
|
||||
: 0),
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: AnimatedScale(
|
||||
scale: videoItem.checked ? 1 : 0,
|
||||
duration:
|
||||
const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
EdgeInsets.zero),
|
||||
backgroundColor:
|
||||
MaterialStateProperty
|
||||
.resolveWith(
|
||||
(states) {
|
||||
return Colors.white
|
||||
.withOpacity(0.8);
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
onChoose!();
|
||||
},
|
||||
icon: Icon(Icons.done_all_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!BusinessType
|
||||
.hiddenDurationType.hiddenDurationType
|
||||
.contains(videoItem.history.business))
|
||||
PBadge(
|
||||
text: videoItem.progress == -1
|
||||
? '已看完'
|
||||
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
// 右上角
|
||||
if (BusinessType.showBadge.showBadge
|
||||
.contains(videoItem.history.business) ||
|
||||
videoItem.history.business ==
|
||||
BusinessType.live.type)
|
||||
PBadge(
|
||||
text: videoItem.badge,
|
||||
top: 6.0,
|
||||
right: 6.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
VideoContent(videoItem: videoItem, ctr: ctr)
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -191,7 +276,8 @@ class HistoryItem extends StatelessWidget {
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const VideoContent({super.key, required this.videoItem});
|
||||
final dynamic ctr;
|
||||
const VideoContent({super.key, required this.videoItem, this.ctr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -211,7 +297,8 @@ class VideoContent extends StatelessWidget {
|
||||
maxLines: videoItem.videos > 1 ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (videoItem.showTitle != null)
|
||||
if (videoItem.showTitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
videoItem.showTitle,
|
||||
textAlign: TextAlign.start,
|
||||
@ -219,21 +306,24 @@ class VideoContent extends StatelessWidget {
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
maxLines: 2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
videoItem.authorName,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
if (videoItem.authorName != '')
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
videoItem.authorName,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -244,26 +334,26 @@ class VideoContent extends StatelessWidget {
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
if (videoItem.badge != '番剧' &&
|
||||
!videoItem.tagName.contains('动画') &&
|
||||
videoItem.history.business != 'live' &&
|
||||
!videoItem.history.business.contains('article'))
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: '稍后再看',
|
||||
icon: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
size: 14,
|
||||
),
|
||||
position: PopupMenuPosition.under,
|
||||
// constraints: const BoxConstraints(maxHeight: 35),
|
||||
onSelected: (String type) {},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: '功能菜单',
|
||||
icon: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
size: 14,
|
||||
),
|
||||
position: PopupMenuPosition.under,
|
||||
// constraints: const BoxConstraints(maxHeight: 35),
|
||||
onSelected: (String type) {},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
if (videoItem.badge != '番剧' &&
|
||||
!videoItem.tagName.contains('动画') &&
|
||||
videoItem.history.business != 'live' &&
|
||||
!videoItem.history.business.contains('article'))
|
||||
PopupMenuItem<String>(
|
||||
onTap: () async {
|
||||
var res = await UserHttp.toViewLater(
|
||||
@ -280,9 +370,22 @@ class VideoContent extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
onTap: () => ctr!.delHistory(
|
||||
videoItem.kid, videoItem.history.business),
|
||||
value: 'pause',
|
||||
height: 35,
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.close_outlined, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text('删除记录', style: TextStyle(fontSize: 13))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
91
lib/pages/history_search/controller.dart
Normal file
91
lib/pages/history_search/controller.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/user/history.dart';
|
||||
|
||||
class HistorySearchController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Rx<TextEditingController> controller = TextEditingController().obs;
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs;
|
||||
String hintText = '搜索';
|
||||
RxString loadingStatus = 'init'.obs;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
bool hasRequest = false;
|
||||
late int mid;
|
||||
RxString uname = ''.obs;
|
||||
int pn = 1;
|
||||
int count = 0;
|
||||
RxList<HisListItem> historyList = <HisListItem>[].obs;
|
||||
RxBool enableMultiple = false.obs;
|
||||
|
||||
// 清空搜索
|
||||
void onClear() {
|
||||
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
|
||||
controller.value.clear();
|
||||
searchKeyWord.value = '';
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
void onChange(value) {
|
||||
searchKeyWord.value = value;
|
||||
}
|
||||
|
||||
// 提交搜索内容
|
||||
void submit() {
|
||||
loadingStatus.value = 'loading';
|
||||
if (hasRequest) {
|
||||
pn = 1;
|
||||
searchHistories();
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索视频
|
||||
Future searchHistories({type = 'init'}) async {
|
||||
if (type == 'onLoad' && loadingText.value == '没有更多了') {
|
||||
return;
|
||||
}
|
||||
var res = await UserHttp.searchHistory(
|
||||
pn: pn,
|
||||
keyword: controller.value.text,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (type == 'init' && pn == 1) {
|
||||
historyList.value = res['data'].list;
|
||||
} else {
|
||||
historyList.addAll(res['data'].list);
|
||||
}
|
||||
count = res['data'].page['total'];
|
||||
if (historyList.length == count) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
pn += 1;
|
||||
hasRequest = true;
|
||||
}
|
||||
loadingStatus.value = 'finish';
|
||||
return res;
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
searchHistories(type: 'onLoad');
|
||||
}
|
||||
|
||||
Future delHistory(kid, business) async {
|
||||
String resKid = 'archive_$kid';
|
||||
if (business == 'live') {
|
||||
resKid = 'live_$kid';
|
||||
} else if (business.contains('article')) {
|
||||
resKid = 'article_$kid';
|
||||
}
|
||||
|
||||
var res = await UserHttp.delHistory(resKid);
|
||||
if (res['status']) {
|
||||
historyList.removeWhere((e) => e.kid == kid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
loadingStatus.value = 'finish';
|
||||
}
|
||||
}
|
||||
4
lib/pages/history_search/index.dart
Normal file
4
lib/pages/history_search/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library history_search;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
174
lib/pages/history_search/view.dart
Normal file
174
lib/pages/history_search/view.dart
Normal file
@ -0,0 +1,174 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/pages/history/widgets/item.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class HistorySearchPage extends StatefulWidget {
|
||||
const HistorySearchPage({super.key});
|
||||
|
||||
@override
|
||||
State<HistorySearchPage> createState() => _HistorySearchPageState();
|
||||
}
|
||||
|
||||
class _HistorySearchPageState extends State<HistorySearchPage> {
|
||||
final HistorySearchController _historySearchCtr =
|
||||
Get.put(HistorySearchController());
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = _historySearchCtr.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
|
||||
_historySearchCtr.onLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _historySearchCtr.submit(),
|
||||
icon: const Icon(Icons.search_outlined, size: 22)),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: Obx(
|
||||
() => TextField(
|
||||
autofocus: true,
|
||||
focusNode: _historySearchCtr.searchFocusNode,
|
||||
controller: _historySearchCtr.controller.value,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (value) => _historySearchCtr.onChange(value),
|
||||
decoration: InputDecoration(
|
||||
hintText: _historySearchCtr.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 22,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
onPressed: () => _historySearchCtr.onClear(),
|
||||
),
|
||||
),
|
||||
onSubmitted: (String value) => _historySearchCtr.submit(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => Column(
|
||||
children: _historySearchCtr.loadingStatus.value == 'init'
|
||||
? [const SizedBox()]
|
||||
: [
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: _historySearchCtr.searchHistories(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => _historySearchCtr.historyList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount:
|
||||
_historySearchCtr.historyList.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
_historySearchCtr
|
||||
.historyList.length) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_historySearchCtr
|
||||
.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HistoryItem(
|
||||
videoItem: _historySearchCtr
|
||||
.historyList[index],
|
||||
ctr: _historySearchCtr,
|
||||
onChoose: null,
|
||||
onUpdateMultiple: () => null,
|
||||
);
|
||||
;
|
||||
}
|
||||
},
|
||||
)
|
||||
: _historySearchCtr.loadingStatus.value ==
|
||||
'loading'
|
||||
? const SizedBox(child: Text('加载中...'))
|
||||
: const CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
NoData(),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
112
lib/pages/html/controller.dart
Normal file
112
lib/pages/html/controller.dart
Normal file
@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/html.dart';
|
||||
import 'package:pilipala/http/reply.dart';
|
||||
import 'package:pilipala/models/common/reply_sort_type.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HtmlRenderController extends GetxController {
|
||||
late String id;
|
||||
late String dynamicType;
|
||||
late int type;
|
||||
RxInt oid = (-1).obs;
|
||||
late Map response;
|
||||
int? floor;
|
||||
int currentPage = 0;
|
||||
bool isLoadingMore = false;
|
||||
RxString noMore = ''.obs;
|
||||
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
|
||||
RxInt acount = 0.obs;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
ReplySortType _sortType = ReplySortType.time;
|
||||
RxString sortTypeTitle = ReplySortType.time.titles.obs;
|
||||
RxString sortTypeLabel = ReplySortType.time.labels.obs;
|
||||
Box setting = GStrorage.setting;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
id = Get.parameters['id']!;
|
||||
dynamicType = Get.parameters['dynamicType']!;
|
||||
type = dynamicType == 'picture' ? 11 : 12;
|
||||
}
|
||||
|
||||
// 请求动态内容
|
||||
Future reqHtml(id) async {
|
||||
late dynamic res;
|
||||
if (dynamicType == 'opus' || dynamicType == 'picture') {
|
||||
res = await HtmlHttp.reqHtml(id, dynamicType);
|
||||
} else {
|
||||
res = await HtmlHttp.reqReadHtml(id, dynamicType);
|
||||
}
|
||||
response = res;
|
||||
oid.value = res['commentId'];
|
||||
return res;
|
||||
}
|
||||
|
||||
// 请求评论
|
||||
Future queryReplyList({reqType = 'init'}) async {
|
||||
var res = await ReplyHttp.replyList(
|
||||
oid: oid.value,
|
||||
pageNum: currentPage + 1,
|
||||
type: type,
|
||||
sort: _sortType.index,
|
||||
);
|
||||
if (res['status']) {
|
||||
List<ReplyItemModel> replies = res['data'].replies;
|
||||
acount.value = res['data'].page.acount;
|
||||
if (replies.isNotEmpty) {
|
||||
currentPage++;
|
||||
noMore.value = '加载中...';
|
||||
if (replies.length < 20) {
|
||||
noMore.value = '没有更多了';
|
||||
}
|
||||
} else {
|
||||
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
|
||||
}
|
||||
if (reqType == 'init') {
|
||||
// 添加置顶回复
|
||||
if (res['data'].upper.top != null) {
|
||||
bool flag = res['data']
|
||||
.topReplies
|
||||
.any((reply) => reply.rpid == res['data'].upper.top.rpid);
|
||||
if (!flag) {
|
||||
replies.insert(0, res['data'].upper.top);
|
||||
}
|
||||
}
|
||||
replies.insertAll(0, res['data'].topReplies);
|
||||
replyList.value = replies;
|
||||
} else {
|
||||
replyList.addAll(replies);
|
||||
}
|
||||
}
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
// 排序搜索评论
|
||||
queryBySort() {
|
||||
feedBack();
|
||||
switch (_sortType) {
|
||||
case ReplySortType.time:
|
||||
_sortType = ReplySortType.like;
|
||||
break;
|
||||
case ReplySortType.like:
|
||||
_sortType = ReplySortType.reply;
|
||||
break;
|
||||
case ReplySortType.reply:
|
||||
_sortType = ReplySortType.time;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
sortTypeTitle.value = _sortType.titles;
|
||||
sortTypeLabel.value = _sortType.labels;
|
||||
currentPage = 0;
|
||||
replyList.clear();
|
||||
queryReplyList(reqType: 'init');
|
||||
}
|
||||
}
|
||||
4
lib/pages/html/index.dart
Normal file
4
lib/pages/html/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library html_render;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
457
lib/pages/html/view.dart
Normal file
457
lib/pages/html/view.dart
Normal file
@ -0,0 +1,457 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_reply.dart';
|
||||
import 'package:pilipala/common/widgets/html_render.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/pages/mine/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class HtmlRenderPage extends StatefulWidget {
|
||||
const HtmlRenderPage({super.key});
|
||||
|
||||
@override
|
||||
State<HtmlRenderPage> createState() => _HtmlRenderPageState();
|
||||
}
|
||||
|
||||
class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
with TickerProviderStateMixin {
|
||||
final HtmlRenderController _htmlRenderCtr = Get.put(HtmlRenderController());
|
||||
late String title;
|
||||
late String id;
|
||||
late String url;
|
||||
late String dynamicType;
|
||||
late int type;
|
||||
bool _isFabVisible = true;
|
||||
late Future _futureBuilderFuture;
|
||||
late ScrollController scrollController;
|
||||
late AnimationController fabAnimationCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
title = Get.parameters['title']!;
|
||||
id = Get.parameters['id']!;
|
||||
url = Get.parameters['url']!;
|
||||
dynamicType = Get.parameters['dynamicType']!;
|
||||
type = dynamicType == 'picture' ? 11 : 12;
|
||||
_futureBuilderFuture = _htmlRenderCtr.reqHtml(id);
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
scrollListener();
|
||||
}
|
||||
|
||||
void scrollListener() {
|
||||
scrollController = _htmlRenderCtr.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
// 分页加载
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_htmlRenderCtr.queryReplyList(reqType: 'onLoad');
|
||||
});
|
||||
}
|
||||
|
||||
// 标题
|
||||
// if (scrollController.offset > 55 && !_visibleTitle) {
|
||||
// _visibleTitle = true;
|
||||
// titleStreamC.add(true);
|
||||
// } else if (scrollController.offset <= 55 && _visibleTitle) {
|
||||
// _visibleTitle = false;
|
||||
// titleStreamC.add(false);
|
||||
// }
|
||||
|
||||
// fab按钮
|
||||
final ScrollDirection direction =
|
||||
scrollController.position.userScrollDirection;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
_showFab();
|
||||
} else if (direction == ScrollDirection.reverse) {
|
||||
_hideFab();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showFab() {
|
||||
if (!_isFabVisible) {
|
||||
_isFabVisible = true;
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _hideFab() {
|
||||
if (_isFabVisible) {
|
||||
_isFabVisible = false;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void replyReply(replyItem) {
|
||||
int oid = replyItem.oid;
|
||||
int rpid = replyItem.rpid!;
|
||||
Get.to(
|
||||
() => Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'评论详情',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
body: VideoReplyReplyPanel(
|
||||
oid: oid,
|
||||
rpid: rpid,
|
||||
source: 'dynamic',
|
||||
replyType: ReplyType.values[type],
|
||||
firstFloor: replyItem,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
titleSpacing: 0,
|
||||
title: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.toNamed('/webview', parameters: {
|
||||
'url': url.startsWith('http') ? url : 'https:$url',
|
||||
'type': 'url',
|
||||
'pageTitle': title,
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.open_in_browser_outlined, size: 19),
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
onTap: () => {
|
||||
Clipboard.setData(ClipboardData(text: url)),
|
||||
SmartDialog.showToast('已复制'),
|
||||
},
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.copy_rounded, size: 19),
|
||||
SizedBox(width: 10),
|
||||
Text('复制链接'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => {},
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.share_outlined, size: 19),
|
||||
SizedBox(width: 10),
|
||||
Text('分享'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 6)
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
var data = snapshot.data;
|
||||
fabAnimationCtr.forward();
|
||||
if (data['status']) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 40,
|
||||
height: 40,
|
||||
type: 'avatar',
|
||||
src: _htmlRenderCtr.response['avatar']!,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(_htmlRenderCtr.response['uname'],
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall!
|
||||
.fontSize,
|
||||
)),
|
||||
Text(
|
||||
_htmlRenderCtr.response['updateTime'],
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||
child: HtmlRender(
|
||||
htmlContent: _htmlRenderCtr.response['content'],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 8,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Text('error');
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
Obx(
|
||||
() => _htmlRenderCtr.oid.value != -1
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 0.6,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('回复'),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: () => _htmlRenderCtr.queryBySort(),
|
||||
icon: const Icon(Icons.sort, size: 16),
|
||||
label: Obx(
|
||||
() => Text(
|
||||
_htmlRenderCtr.sortTypeLabel.value,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Obx(
|
||||
() => _htmlRenderCtr.oid.value != -1
|
||||
? FutureBuilder(
|
||||
future: _htmlRenderCtr.queryReplyList(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => _htmlRenderCtr.replyList.isEmpty &&
|
||||
_htmlRenderCtr.isLoadingMore
|
||||
? ListView.builder(
|
||||
itemCount: 5,
|
||||
shrinkWrap: true,
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
},
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(),
|
||||
itemCount:
|
||||
_htmlRenderCtr.replyList.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
_htmlRenderCtr
|
||||
.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_htmlRenderCtr
|
||||
.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ReplyItem(
|
||||
replyItem: _htmlRenderCtr
|
||||
.replyList[index],
|
||||
showReplyRow: true,
|
||||
replyLevel: '1',
|
||||
replyReply: (replyItem) =>
|
||||
replyReply(replyItem),
|
||||
replyType:
|
||||
ReplyType.values[type],
|
||||
addReply: (replyItem) {
|
||||
_htmlRenderCtr
|
||||
.replyList[index].replies!
|
||||
.add(replyItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 14,
|
||||
right: 14,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 2),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: fabAnimationCtr,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return VideoReplyNewDialog(
|
||||
oid: _htmlRenderCtr.oid.value,
|
||||
root: 0,
|
||||
parent: 0,
|
||||
replyType: ReplyType.values[type],
|
||||
);
|
||||
},
|
||||
).then(
|
||||
(value) => {
|
||||
// 完成评论,数据添加
|
||||
if (value != null && value['data'] != null)
|
||||
{
|
||||
_htmlRenderCtr.replyList.add(value['data']),
|
||||
_htmlRenderCtr.acount.value++
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
tooltip: '评论动态',
|
||||
child: const Icon(Icons.reply),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -20,14 +20,14 @@ class LiveController extends GetxController {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
crossAxisCount.value =
|
||||
setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2;
|
||||
setting.get(SettingBoxKey.customRows, defaultValue: 2);
|
||||
}
|
||||
|
||||
// 获取推荐
|
||||
Future queryLiveList(type) async {
|
||||
if (type == 'init') {
|
||||
_currentPage = 1;
|
||||
}
|
||||
// if (type == 'init') {
|
||||
// _currentPage = 1;
|
||||
// }
|
||||
var res = await LiveHttp.liveList(
|
||||
pn: _currentPage,
|
||||
);
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/overlay_pop.dart';
|
||||
import 'package:pilipala/pages/main/index.dart';
|
||||
import 'package:pilipala/pages/rcmd/index.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'widgets/live_item.dart';
|
||||
@ -21,11 +22,15 @@ class LivePage extends StatefulWidget {
|
||||
State<LivePage> createState() => _LivePageState();
|
||||
}
|
||||
|
||||
class _LivePageState extends State<LivePage> {
|
||||
class _LivePageState extends State<LivePage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final LiveController _liveController = Get.put(LiveController());
|
||||
late Future _futureBuilderFuture;
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -37,7 +42,7 @@ class _LivePageState extends State<LivePage> {
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
|
||||
EasyThrottle.throttle('liveList', const Duration(seconds: 1), () {
|
||||
_liveController.isLoadingMore = true;
|
||||
_liveController.onLoad();
|
||||
});
|
||||
@ -84,6 +89,9 @@ class _LivePageState extends State<LivePage> {
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return SliverLayoutBuilder(
|
||||
@ -111,7 +119,7 @@ class _LivePageState extends State<LivePage> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const LoadingMore()
|
||||
LoadingMore(ctr: _liveController)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -141,9 +149,9 @@ class _LivePageState extends State<LivePage> {
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
// 行间距
|
||||
mainAxisSpacing: StyleString.cardSpace + 4,
|
||||
mainAxisSpacing: StyleString.safeSpace,
|
||||
// 列间距
|
||||
crossAxisSpacing: StyleString.cardSpace + 4,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
// 列数
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisExtent:
|
||||
@ -173,24 +181,3 @@ class _LivePageState extends State<LivePage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingMore extends StatelessWidget {
|
||||
const LoadingMore({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).padding.bottom + 80,
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'加载中...',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline, fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ class LiveCardV extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
String heroTag = Utils.makeHeroTag(liveItem.roomId);
|
||||
return Card(
|
||||
elevation: crossAxisCount == 1 ? 0 : 1,
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: GestureDetector(
|
||||
@ -102,7 +102,7 @@ class LiveContent extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: crossAxisCount == 1
|
||||
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
|
||||
: const EdgeInsets.fromLTRB(9, 8, 9, 8),
|
||||
: const EdgeInsets.fromLTRB(5, 8, 5, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -120,15 +120,18 @@ class LiveContent extends StatelessWidget {
|
||||
if (crossAxisCount == 1) const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
liveItem.uname,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
Expanded(
|
||||
child: Text(
|
||||
liveItem.uname,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (crossAxisCount == 1) ...[
|
||||
Text(
|
||||
@ -169,7 +172,7 @@ class VideoStat extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(top: 22, left: 10, right: 10),
|
||||
padding: const EdgeInsets.only(top: 26, left: 10, right: 10),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
@ -181,18 +184,17 @@ class VideoStat extends StatelessWidget {
|
||||
tileMode: TileMode.mirror,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
liveItem!.areaName!,
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
liveItem!.watchedShow!['text_small'],
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
),
|
||||
],
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.justify,
|
||||
softWrap: false,
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
children: [
|
||||
TextSpan(text: liveItem!.areaName!),
|
||||
TextSpan(text: liveItem!.watchedShow!['text_small']),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'widgets/bottom_control.dart';
|
||||
|
||||
class LiveRoomPage extends StatefulWidget {
|
||||
const LiveRoomPage({super.key});
|
||||
@ -18,6 +22,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
|
||||
bool isShowCover = true;
|
||||
bool isPlay = true;
|
||||
Floating? floating;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -31,19 +36,24 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
}
|
||||
},
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
floating = Floating();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
plPlayerController!.dispose();
|
||||
if (floating != null) {
|
||||
floating!.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||
|
||||
return Scaffold(
|
||||
Widget childWhenDisabled = Scaffold(
|
||||
primary: true,
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
@ -87,98 +97,61 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Hero(
|
||||
tag: _liveRoomController.heroTag,
|
||||
child: Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: plPlayerController!.videoPlayerController != null
|
||||
? PLVideoPlayer(controller: plPlayerController!)
|
||||
: const SizedBox(),
|
||||
),
|
||||
// if (_liveRoomController.liveItem != null &&
|
||||
// _liveRoomController.liveItem.cover != null)
|
||||
// Visibility(
|
||||
// visible: isShowCover,
|
||||
// child: Positioned(
|
||||
// top: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: NetworkImgLayer(
|
||||
// type: 'emote',
|
||||
// src: _liveRoomController.liveItem.cover,
|
||||
// width: Get.size.width,
|
||||
// height: videoHeight,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: plPlayerController!.videoPlayerController != null
|
||||
? PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
bottomControl: BottomControl(
|
||||
controller: plPlayerController,
|
||||
liveRoomCtr: _liveRoomController,
|
||||
floating: floating,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
// if (_liveRoomController.liveItem != null &&
|
||||
// _liveRoomController.liveItem.cover != null)
|
||||
// Visibility(
|
||||
// visible: isShowCover,
|
||||
// child: Positioned(
|
||||
// top: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: NetworkImgLayer(
|
||||
// type: 'emote',
|
||||
// src: _liveRoomController.liveItem.cover,
|
||||
// width: Get.size.width,
|
||||
// height: videoHeight,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
// Container(
|
||||
// height: 45,
|
||||
// padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Theme.of(context).colorScheme.background,
|
||||
// border: Border(
|
||||
// bottom: BorderSide(
|
||||
// color: Theme.of(context).dividerColor.withOpacity(0.1)),
|
||||
// ),
|
||||
// ),
|
||||
// child: Row(children: <Widget>[
|
||||
// SizedBox(
|
||||
// width: 38,
|
||||
// height: 38,
|
||||
// child: IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(
|
||||
// Icons.subtitles_outlined,
|
||||
// size: 21,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const Spacer(),
|
||||
// SizedBox(
|
||||
// width: 38,
|
||||
// height: 38,
|
||||
// child: IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(
|
||||
// Icons.hd_outlined,
|
||||
// size: 20,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 38,
|
||||
// height: 38,
|
||||
// child: IconButton(
|
||||
// onPressed: () => _liveRoomController
|
||||
// .setVolumn(plPlayerController!.volume.value),
|
||||
// icon: Obx(() => Icon(
|
||||
// _liveRoomController.volumeOff.value
|
||||
// ? Icons.volume_off_outlined
|
||||
// : Icons.volume_up_outlined,
|
||||
// size: 21,
|
||||
// )),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 38,
|
||||
// height: 38,
|
||||
// child: IconButton(
|
||||
// onPressed: () => {},
|
||||
// // plPlayerController!.goToFullscreen(context),
|
||||
// icon: const Icon(
|
||||
// Icons.fullscreen,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ]),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget childWhenEnabled = AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: plPlayerController!.videoPlayerController != null
|
||||
? PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
bottomControl: BottomControl(
|
||||
controller: plPlayerController,
|
||||
liveRoomCtr: _liveRoomController,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
return PiPSwitcher(
|
||||
childWhenDisabled: childWhenDisabled,
|
||||
childWhenEnabled: childWhenEnabled,
|
||||
);
|
||||
} else {
|
||||
return childWhenDisabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
lib/pages/liveRoom/widgets/bottom_control.dart
Normal file
151
lib/pages/liveRoom/widgets/bottom_control.dart
Normal file
@ -0,0 +1,151 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/video/play/url.dart';
|
||||
import 'package:pilipala/pages/liveRoom/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class BottomControl extends StatefulWidget implements PreferredSizeWidget {
|
||||
final PlPlayerController? controller;
|
||||
final LiveRoomController? liveRoomCtr;
|
||||
final Floating? floating;
|
||||
const BottomControl({
|
||||
this.controller,
|
||||
this.liveRoomCtr,
|
||||
this.floating,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BottomControl> createState() => _BottomControlState();
|
||||
|
||||
@override
|
||||
Size get preferredSize => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class _BottomControlState extends State<BottomControl> {
|
||||
late PlayUrlModel videoInfo;
|
||||
List<PlaySpeed> playSpeed = PlaySpeed.values;
|
||||
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
|
||||
TextStyle titleStyle = const TextStyle(fontSize: 14);
|
||||
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
|
||||
Box localCache = GStrorage.localCache;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const textStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
);
|
||||
return AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
primary: false,
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 14,
|
||||
title: Row(
|
||||
children: [
|
||||
// ComBtn(
|
||||
// icon: const Icon(
|
||||
// Icons.subtitles_outlined,
|
||||
// size: 18,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// fuc: () => Get.back(),
|
||||
// ),
|
||||
const Spacer(),
|
||||
// ComBtn(
|
||||
// icon: const Icon(
|
||||
// Icons.hd_outlined,
|
||||
// size: 18,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// fuc: () => {},
|
||||
// ),
|
||||
// const SizedBox(width: 4),
|
||||
// Obx(
|
||||
// () => ComBtn(
|
||||
// icon: Icon(
|
||||
// widget.liveRoomCtr!.volumeOff.value
|
||||
// ? Icons.volume_off_outlined
|
||||
// : Icons.volume_up_outlined,
|
||||
// size: 18,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// fuc: () => {},
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 4),
|
||||
if (Platform.isAndroid) ...[
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () async {
|
||||
bool canUsePiP = false;
|
||||
widget.controller!.hiddenControls(false);
|
||||
try {
|
||||
canUsePiP = await widget.floating!.isPipAvailable;
|
||||
} on PlatformException catch (_) {
|
||||
canUsePiP = false;
|
||||
}
|
||||
if (canUsePiP) {
|
||||
await widget.floating!.enable();
|
||||
} else {}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_outlined,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
Icons.fullscreen,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
fuc: () => widget.controller!.triggerFullScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MSliderTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
SliderThemeData? sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
const double trackHeight = 3;
|
||||
final double trackLeft = offset.dx;
|
||||
final double trackTop =
|
||||
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
|
||||
final double trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
204
lib/pages/login/controller.dart
Normal file
204
lib/pages/login/controller.dart
Normal file
@ -0,0 +1,204 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/login.dart';
|
||||
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
|
||||
import 'package:pilipala/models/login/index.dart';
|
||||
|
||||
class LoginPageController extends GetxController {
|
||||
final GlobalKey mobFormKey = GlobalKey<FormState>();
|
||||
final GlobalKey passwordFormKey = GlobalKey<FormState>();
|
||||
final GlobalKey msgCodeFormKey = GlobalKey<FormState>();
|
||||
|
||||
final TextEditingController mobTextController = TextEditingController();
|
||||
final TextEditingController passwordTextController = TextEditingController();
|
||||
final TextEditingController msgCodeTextController = TextEditingController();
|
||||
|
||||
final FocusNode mobTextFieldNode = FocusNode();
|
||||
final FocusNode passwordTextFieldNode = FocusNode();
|
||||
final FocusNode msgCodeTextFieldNode = FocusNode();
|
||||
|
||||
final PageController pageViewController = PageController();
|
||||
|
||||
RxInt currentIndex = 0.obs;
|
||||
|
||||
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
|
||||
|
||||
// 默认密码登录
|
||||
RxInt loginType = 0.obs;
|
||||
|
||||
// 监听pageView切换
|
||||
void onPageChange(int index) {
|
||||
currentIndex.value = index;
|
||||
}
|
||||
|
||||
// 输入手机号 下一页
|
||||
void nextStep() async {
|
||||
if ((mobFormKey.currentState as FormState).validate()) {
|
||||
await pageViewController.animateToPage(
|
||||
1,
|
||||
duration: const Duration(microseconds: 3000),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
passwordTextFieldNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// 上一页
|
||||
void previousPage() async {
|
||||
passwordTextFieldNode.unfocus();
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
pageViewController.animateToPage(
|
||||
0,
|
||||
duration: const Duration(microseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
// 切换登录方式
|
||||
void changeLoginType() {
|
||||
loginType.value = loginType.value == 0 ? 1 : 0;
|
||||
if (loginType.value == 0) {
|
||||
passwordTextFieldNode.requestFocus();
|
||||
} else {
|
||||
msgCodeTextFieldNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// app端密码登录
|
||||
void loginInByAppPassword() async {
|
||||
if ((passwordFormKey.currentState as FormState).validate()) {
|
||||
var webKeyRes = await LoginHttp.getWebKey();
|
||||
if (webKeyRes['status']) {
|
||||
String rhash = webKeyRes['data']['hash'];
|
||||
String key = webKeyRes['data']['key'];
|
||||
LoginHttp.loginInByMobPwd(
|
||||
tel: mobTextController.text,
|
||||
password: passwordTextController.text,
|
||||
key: key,
|
||||
rhash: rhash,
|
||||
);
|
||||
} else {
|
||||
SmartDialog.showToast(webKeyRes['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证码登录
|
||||
void loginInByCode() {
|
||||
if ((msgCodeFormKey.currentState as FormState).validate()) {}
|
||||
}
|
||||
|
||||
// app端验证码
|
||||
void getMsgCode() async {
|
||||
getCaptcha((data) async {
|
||||
CaptchaDataModel captchaData = data;
|
||||
var res = await LoginHttp.sendAppSmsCode(
|
||||
cid: 86,
|
||||
tel: 13734077064,
|
||||
token: captchaData.token!,
|
||||
challenge: captchaData.geetest!.challenge!,
|
||||
validate: captchaData.validate!,
|
||||
seccode: captchaData.seccode!,
|
||||
);
|
||||
print(res);
|
||||
});
|
||||
}
|
||||
|
||||
// 申请极验验证码
|
||||
Future getCaptcha(oncall) async {
|
||||
SmartDialog.showLoading(msg: '请求中...');
|
||||
var result = await LoginHttp.queryCaptcha();
|
||||
if (result['status']) {
|
||||
CaptchaDataModel captchaData = result['data'];
|
||||
var registerData = Gt3RegisterData(
|
||||
challenge: captchaData.geetest!.challenge,
|
||||
gt: captchaData.geetest!.gt!,
|
||||
success: true,
|
||||
);
|
||||
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
|
||||
SmartDialog.dismiss();
|
||||
}, onClose: (Map<String, dynamic> message) async {
|
||||
SmartDialog.showToast('关闭验证');
|
||||
}, onResult: (Map<String, dynamic> message) async {
|
||||
debugPrint("Captcha result: $message");
|
||||
String code = message["code"];
|
||||
if (code == "1") {
|
||||
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
|
||||
SmartDialog.showToast('验证成功');
|
||||
captchaData.validate = message['result']['geetest_validate'];
|
||||
captchaData.seccode = message['result']['geetest_seccode'];
|
||||
captchaData.geetest!.challenge =
|
||||
message['result']['geetest_challenge'];
|
||||
oncall(captchaData);
|
||||
} else {
|
||||
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
|
||||
debugPrint("Captcha result code : $code");
|
||||
}
|
||||
}, onError: (Map<String, dynamic> message) async {
|
||||
String code = message["code"];
|
||||
|
||||
// 处理验证中返回的错误 Handling errors returned in verification
|
||||
if (Platform.isAndroid) {
|
||||
// Android 平台
|
||||
if (code == "-2") {
|
||||
// Dart 调用异常 Call exception
|
||||
} else if (code == "-1") {
|
||||
// Gt3RegisterData 参数不合法 Parameter is invalid
|
||||
} else if (code == "201") {
|
||||
// 网络无法访问 Network inaccessible
|
||||
} else if (code == "202") {
|
||||
// Json 解析错误 Analysis error
|
||||
} else if (code == "204") {
|
||||
// WebView 加载超时,请检查是否混淆极验 SDK Load timed out
|
||||
} else if (code == "204_1") {
|
||||
// WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log
|
||||
} else if (code == "204_2") {
|
||||
// WebView 加载 SSLError
|
||||
} else if (code == "206") {
|
||||
// gettype 接口错误或返回为 null API error or return null
|
||||
} else if (code == "207") {
|
||||
// getphp 接口错误或返回为 null API error or return null
|
||||
} else if (code == "208") {
|
||||
// ajax 接口错误或返回为 null API error or return null
|
||||
} else {
|
||||
// 更多错误码参考开发文档 More error codes refer to the development document
|
||||
// https://docs.geetest.com/sensebot/apirefer/errorcode/android
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// iOS 平台
|
||||
if (code == "-1009") {
|
||||
// 网络无法访问 Network inaccessible
|
||||
} else if (code == "-1004") {
|
||||
// 无法查找到 HOST Unable to find HOST
|
||||
} else if (code == "-1002") {
|
||||
// 非法的 URL Illegal URL
|
||||
} else if (code == "-1001") {
|
||||
// 网络超时 Network timeout
|
||||
} else if (code == "-999") {
|
||||
// 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation
|
||||
} else if (code == "-21") {
|
||||
// 使用了重复的 challenge Duplicate challenges are used
|
||||
// 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached
|
||||
} else if (code == "-20") {
|
||||
// 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again
|
||||
} else if (code == "-10") {
|
||||
// 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification
|
||||
} else if (code == "-2") {
|
||||
// Dart 调用异常 Call exception
|
||||
} else if (code == "-1") {
|
||||
// Gt3RegisterData 参数不合法 Parameter is invalid
|
||||
} else {
|
||||
// 更多错误码参考开发文档 More error codes refer to the development document
|
||||
// https://docs.geetest.com/sensebot/apirefer/errorcode/ios
|
||||
}
|
||||
}
|
||||
});
|
||||
captcha.startCaptcha(registerData);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
4
lib/pages/login/index.dart
Normal file
4
lib/pages/login/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library login;
|
||||
|
||||
export './controller.dart';
|
||||
export 'view.dart';
|
||||
362
lib/pages/login/view.dart
Normal file
362
lib/pages/login/view.dart
Normal file
@ -0,0 +1,362 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final LoginPageController _loginPageCtr = Get.put(LoginPageController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: Obx(
|
||||
() => _loginPageCtr.currentIndex.value == 0
|
||||
? IconButton(
|
||||
onPressed: () async {
|
||||
_loginPageCtr.mobTextFieldNode.unfocus();
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () => _loginPageCtr.previousPage(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: PageView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _loginPageCtr.pageViewController,
|
||||
onPageChanged: (int index) => _loginPageCtr.onPageChange(index),
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 10,
|
||||
),
|
||||
child: Form(
|
||||
key: _loginPageCtr.mobFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
'登录',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
letterSpacing: 1,
|
||||
height: 2.1,
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'请使用您的 BiliBili 账号登录。',
|
||||
style: Theme.of(context).textTheme.titleSmall!,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: const Icon(Icons.info_outline, size: 16),
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 38, bottom: 15),
|
||||
child: TextFormField(
|
||||
controller: _loginPageCtr.mobTextController,
|
||||
focusNode: _loginPageCtr.mobTextFieldNode,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: '输入手机号码',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
// 校验用户名
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty ? null : "手机号码不能为空";
|
||||
},
|
||||
onSaved: (val) {
|
||||
print(val);
|
||||
},
|
||||
onEditingComplete: () {
|
||||
_loginPageCtr.nextStep();
|
||||
},
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Get.offNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://passport.bilibili.com/h5-app/passport/login',
|
||||
'type': 'login',
|
||||
'pageTitle': '登录bilibili',
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2),
|
||||
child: Text(
|
||||
'使用网页端登录',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(onPressed: () {}, child: const Text('中国大陆')),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary, // 设置按钮背景色
|
||||
),
|
||||
onPressed: () => _loginPageCtr.nextStep(),
|
||||
child: const Text('下一步'),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 10,
|
||||
),
|
||||
child: Obx(
|
||||
() => _loginPageCtr.loginType.value == 0
|
||||
? Form(
|
||||
key: _loginPageCtr.passwordFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'密码登录',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(
|
||||
letterSpacing: 1,
|
||||
height: 2.1,
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.resolveWith(
|
||||
(states) {
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.1);
|
||||
}),
|
||||
),
|
||||
onPressed: () =>
|
||||
_loginPageCtr.changeLoginType(),
|
||||
icon: const Icon(Icons.swap_vert_outlined),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'请输入您的 BiliBili 密码。',
|
||||
style: Theme.of(context).textTheme.titleSmall!,
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 38, bottom: 15),
|
||||
child: TextFormField(
|
||||
controller: _loginPageCtr.passwordTextController,
|
||||
focusNode: _loginPageCtr.passwordTextFieldNode,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: '输入密码',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
// 校验用户名
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty ? null : "密码不能为空";
|
||||
},
|
||||
onSaved: (val) {
|
||||
print(val);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _loginPageCtr.previousPage(),
|
||||
child: const Text('上一步'),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, // 设置按钮背景色
|
||||
),
|
||||
onPressed: () =>
|
||||
_loginPageCtr.loginInByAppPassword(),
|
||||
child: const Text('确认登录'),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Form(
|
||||
key: _loginPageCtr.msgCodeFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'验证码登录',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(
|
||||
letterSpacing: 1,
|
||||
height: 2.1,
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.resolveWith(
|
||||
(states) {
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.1);
|
||||
}),
|
||||
),
|
||||
onPressed: () =>
|
||||
_loginPageCtr.changeLoginType(),
|
||||
icon: const Icon(Icons.swap_vert_outlined),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'请输入收到到验证码。',
|
||||
style: Theme.of(context).textTheme.titleSmall!,
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 38, bottom: 15),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller:
|
||||
_loginPageCtr.msgCodeTextController,
|
||||
focusNode: _loginPageCtr.msgCodeTextFieldNode,
|
||||
maxLength: 6,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: '输入验证码',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
// 校验用户名
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty
|
||||
? null
|
||||
: "验证码不能为空";
|
||||
},
|
||||
onSaved: (val) {
|
||||
print(val);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 4,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
_loginPageCtr.getMsgCode(),
|
||||
child: const Text('获取验证码'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _loginPageCtr.previousPage(),
|
||||
child: const Text('上一步'),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, // 设置按钮背景色
|
||||
),
|
||||
onPressed: () => _loginPageCtr.loginInByCode(),
|
||||
child: const Text('确认登录'),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
late Animation<double>? _slideAnimation;
|
||||
int selectedIndex = 0;
|
||||
int? _lastSelectTime; //上次点击时间
|
||||
Box setting = GStrorage.setting;
|
||||
late bool enableMYBar;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -45,6 +47,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
|
||||
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
||||
_pageController = PageController(initialPage: selectedIndex);
|
||||
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
|
||||
}
|
||||
|
||||
void setIndex(int value) async {
|
||||
@ -144,21 +147,38 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
builder: (context, AsyncSnapshot snapshot) {
|
||||
return AnimatedSlide(
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
offset: Offset(0, snapshot.data ? 0 : 1),
|
||||
child: NavigationBar(
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: <Widget>[
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: e['icon'],
|
||||
selectedIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
child: enableMYBar
|
||||
? NavigationBar(
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: <Widget>[
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: e['icon'],
|
||||
selectedIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
)
|
||||
: BottomNavigationBar(
|
||||
currentIndex: selectedIndex,
|
||||
onTap: (value) => setIndex(value),
|
||||
iconSize: 16,
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
items: [
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: e['icon'],
|
||||
activeIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@ -39,45 +39,47 @@ class _MediaPageState extends State<MediaPage>
|
||||
Color primary = Theme.of(context).colorScheme.primary;
|
||||
return Scaffold(
|
||||
appBar: AppBar(toolbarHeight: 30),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: null,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Text(
|
||||
'媒体库',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (var i in mediaController.list) ...[
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () => i['onTap'](),
|
||||
dense: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Icon(
|
||||
i['icon'],
|
||||
color: primary,
|
||||
leading: null,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Text(
|
||||
'媒体库',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 15, top: 2, bottom: 2),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(
|
||||
i['title'],
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
for (var i in mediaController.list) ...[
|
||||
ListTile(
|
||||
onTap: () => i['onTap'](),
|
||||
dense: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Icon(
|
||||
i['icon'],
|
||||
color: primary,
|
||||
),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 15, top: 2, bottom: 2),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(
|
||||
i['title'],
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
],
|
||||
Obx(() => mediaController.userLogin.value
|
||||
? favFolder(mediaController, context)
|
||||
: const SizedBox())
|
||||
],
|
||||
Obx(() => mediaController.userLogin.value
|
||||
? favFolder(mediaController, context)
|
||||
: const SizedBox())
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -136,11 +138,14 @@ class _MediaPageState extends State<MediaPage>
|
||||
// const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 170 * MediaQuery.of(context).textScaleFactor,
|
||||
height: 200 * MediaQuery.of(context).textScaleFactor,
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
List favFolderList =
|
||||
|
||||
@ -2,23 +2,43 @@ import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
|
||||
class ArchiveController extends GetxController {
|
||||
ArchiveController(this.mid);
|
||||
int? mid;
|
||||
int pn = 1;
|
||||
int count = 0;
|
||||
RxMap<String, String> currentOrder = <String, String>{}.obs;
|
||||
List<Map<String, String>> orderList = [
|
||||
{'type': 'pubdate', 'label': '最新发布'},
|
||||
{'type': 'click', 'label': '最多播放'},
|
||||
{'type': 'stow', 'label': '最多收藏'},
|
||||
];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
mid ??= int.parse(Get.parameters['mid']!);
|
||||
print('🐶🐶: $mid');
|
||||
currentOrder.value = orderList.first;
|
||||
}
|
||||
|
||||
// 获取用户投稿
|
||||
Future getMemberArchive() async {
|
||||
var res = await MemberHttp.memberArchive(mid: mid, pn: pn);
|
||||
var res = await MemberHttp.memberArchive(
|
||||
mid: mid, pn: pn, order: currentOrder['type']!);
|
||||
if (res['status']) {
|
||||
count = res['data'].page['count'];
|
||||
pn += 1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toggleSort() async {
|
||||
pn = 1;
|
||||
int index = orderList.indexOf(currentOrder.value);
|
||||
if (index == orderList.length - 1) {
|
||||
currentOrder.value = orderList.first;
|
||||
} else {
|
||||
currentOrder.value = orderList[index + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,10 +5,12 @@ import 'package:loading_more_list/loading_more_list.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
import 'package:pilipala/models/member/archive.dart';
|
||||
import 'package:pilipala/pages/member/archive/index.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||
|
||||
class ArchivePanel extends StatefulWidget {
|
||||
const ArchivePanel({super.key});
|
||||
final int? mid;
|
||||
const ArchivePanel({super.key, this.mid});
|
||||
|
||||
@override
|
||||
State<ArchivePanel> createState() => _ArchivePanelState();
|
||||
@ -17,11 +19,21 @@ class ArchivePanel extends StatefulWidget {
|
||||
class _ArchivePanelState extends State<ArchivePanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
DateTime lastRefreshTime = DateTime.now();
|
||||
late final LoadMoreListSource source = LoadMoreListSource();
|
||||
late final LoadMoreListSource source;
|
||||
late final ArchiveController _archiveController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print('🐶🐶: ${widget.mid}');
|
||||
_archiveController = Get.put(ArchiveController(widget.mid),
|
||||
tag: Utils.makeHeroTag(widget.mid));
|
||||
source = LoadMoreListSource(_archiveController);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@ -40,14 +52,63 @@ class _ArchivePanelState extends State<ArchivePanel>
|
||||
// return PullToRefreshHeader(info, lastRefreshTime);
|
||||
// },
|
||||
// ),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 14, top: 8, bottom: 8, right: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('排序方式'),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
width: 85,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () {
|
||||
// _archiveController.order = 'click';
|
||||
// _archiveController.pn = 1;
|
||||
_archiveController.toggleSort();
|
||||
source.refresh(true);
|
||||
// LoadMoreListSource().loadData();
|
||||
},
|
||||
child: Obx(
|
||||
() => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
_archiveController.currentOrder['label']!,
|
||||
key: ValueKey<String>(
|
||||
_archiveController.currentOrder['label']!),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LoadingMoreList<VListItemModel>(
|
||||
ListConfig<VListItemModel>(
|
||||
sourceList: source,
|
||||
itemBuilder:
|
||||
(BuildContext c, VListItemModel item, int index) {
|
||||
return VideoCardH(videoItem: item);
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
VideoCardH(videoItem: item)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return VideoCardH(videoItem: item);
|
||||
}
|
||||
},
|
||||
indicatorBuilder: _buildIndicator,
|
||||
),
|
||||
@ -142,14 +203,18 @@ class _ArchivePanelState extends State<ArchivePanel>
|
||||
}
|
||||
|
||||
class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
|
||||
final ArchiveController _archiveController =
|
||||
Get.put(ArchiveController(), tag: Get.arguments['heroTag']);
|
||||
late ArchiveController ctr;
|
||||
LoadMoreListSource(this.ctr);
|
||||
bool forceRefresh = false;
|
||||
|
||||
@override
|
||||
Future<bool> loadData([bool isloadMoreAction = false]) async {
|
||||
bool isSuccess = false;
|
||||
var res = await _archiveController.getMemberArchive();
|
||||
var res = await ctr.getMemberArchive();
|
||||
if (res['status']) {
|
||||
if (ctr.pn == 2) {
|
||||
clear();
|
||||
}
|
||||
addAll(res['data'].list.vlist);
|
||||
}
|
||||
if (length < res['data'].page['count']) {
|
||||
@ -159,4 +224,17 @@ class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
|
||||
}
|
||||
return isSuccess;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> refresh([bool clearBeforeRequest = false]) async {
|
||||
// _hasMore = true;
|
||||
// pageindex = 1;
|
||||
// //force to refresh list when you don't want clear list before request
|
||||
// //for the case, if your list already has 20 items.
|
||||
forceRefresh = !clearBeforeRequest;
|
||||
var result = await super.refresh(clearBeforeRequest);
|
||||
|
||||
forceRefresh = false;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,22 +3,26 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/member/archive.dart';
|
||||
import 'package:pilipala/models/member/info.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class MemberController extends GetxController {
|
||||
late int mid;
|
||||
Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs;
|
||||
Map? userStat;
|
||||
String? face;
|
||||
RxString face = ''.obs;
|
||||
String? heroTag;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
late int ownerMid;
|
||||
// 投稿列表
|
||||
RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
|
||||
var userInfo;
|
||||
RxInt attribute = (-1).obs;
|
||||
RxString attributeText = '关注'.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -26,8 +30,9 @@ class MemberController extends GetxController {
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
ownerMid = userInfo != null ? userInfo.mid : -1;
|
||||
face = Get.arguments['face'] ?? '';
|
||||
face.value = Get.arguments['face'] ?? '';
|
||||
heroTag = Get.arguments['heroTag'] ?? '';
|
||||
relationSearch();
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
@ -36,6 +41,7 @@ class MemberController extends GetxController {
|
||||
var res = await MemberHttp.memberInfo(mid: mid);
|
||||
if (res['status']) {
|
||||
memberInfo.value = res['data'];
|
||||
face.value = res['data'].face;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@ -63,7 +69,10 @@ class MemberController extends GetxController {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
|
||||
if (attribute.value == 128) {
|
||||
blockUser();
|
||||
return;
|
||||
}
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
@ -73,8 +82,12 @@ class MemberController extends GetxController {
|
||||
content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: const Text('点错了')),
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: Text(
|
||||
'点错了',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await VideoHttp.relationMod(
|
||||
@ -83,8 +96,7 @@ class MemberController extends GetxController {
|
||||
reSrc: 11,
|
||||
);
|
||||
memberInfo.value.isFollowed = !memberInfo.value.isFollowed!;
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showLoading();
|
||||
relationSearch();
|
||||
SmartDialog.dismiss();
|
||||
memberInfo.update((val) {});
|
||||
},
|
||||
@ -95,4 +107,70 @@ class MemberController extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 关系查询
|
||||
Future relationSearch() async {
|
||||
if (userInfo == null) return;
|
||||
if (mid == ownerMid) return;
|
||||
var res = await UserHttp.relationSearch(mid);
|
||||
if (res['status']) {
|
||||
attribute.value = res['data']['relation']['attribute'];
|
||||
attributeText.value = attribute.value == 0
|
||||
? '关注'
|
||||
: attribute.value == 2
|
||||
? '已关注'
|
||||
: attribute.value == 6
|
||||
? '已互粉'
|
||||
: '已拉黑';
|
||||
}
|
||||
}
|
||||
|
||||
// 拉黑用户
|
||||
Future blockUser() async {
|
||||
if (userInfo == null) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: Text(attribute.value != 128 ? '确定拉黑UP主?' : '从黑名单移除UP主'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: Text(
|
||||
'点错了',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var res = await VideoHttp.relationMod(
|
||||
mid: mid,
|
||||
act: attribute.value != 128 ? 5 : 6,
|
||||
reSrc: 11,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (res['status']) {
|
||||
attribute.value = attribute.value != 128 ? 128 : 0;
|
||||
attributeText.value = attribute.value == 128 ? '已拉黑' : '关注';
|
||||
memberInfo.value.isFollowed = false;
|
||||
relationSearch();
|
||||
memberInfo.update((val) {});
|
||||
}
|
||||
},
|
||||
child: const Text('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void shareUser() {
|
||||
Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid');
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,23 +2,29 @@ import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
|
||||
class MemberDynamicPanelController extends GetxController {
|
||||
MemberDynamicPanelController(this.mid);
|
||||
int? mid;
|
||||
String offset = '';
|
||||
int count = 0;
|
||||
bool hasMore = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
mid ??= int.parse(Get.parameters['mid']!);
|
||||
}
|
||||
|
||||
Future getMemberDynamic() async {
|
||||
if (!hasMore) {
|
||||
return {'status': false};
|
||||
}
|
||||
var res = await MemberHttp.memberDynamic(
|
||||
offset: offset,
|
||||
mid: mid,
|
||||
);
|
||||
if (res['status']) {
|
||||
offset = res['data'].offset;
|
||||
hasMore = res['data'].hasMore;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -4,11 +4,13 @@ import 'package:get/get.dart';
|
||||
import 'package:loading_more_list/loading_more_list.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/pages/dynamics/widgets/dynamic_panel.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class MemberDynamicPanel extends StatefulWidget {
|
||||
const MemberDynamicPanel({super.key});
|
||||
final int? mid;
|
||||
const MemberDynamicPanel({super.key, this.mid});
|
||||
|
||||
@override
|
||||
State<MemberDynamicPanel> createState() => _MemberDynamicPanelState();
|
||||
@ -17,11 +19,20 @@ class MemberDynamicPanel extends StatefulWidget {
|
||||
class _MemberDynamicPanelState extends State<MemberDynamicPanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
DateTime lastRefreshTime = DateTime.now();
|
||||
late final LoadMoreListSource source = LoadMoreListSource();
|
||||
late final LoadMoreListSource source;
|
||||
late final MemberDynamicPanelController _dynamicController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_dynamicController = Get.put(MemberDynamicPanelController(widget.mid),
|
||||
tag: Utils.makeHeroTag(widget.mid));
|
||||
source = LoadMoreListSource(_dynamicController);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@ -118,21 +129,24 @@ class _MemberDynamicPanelState extends State<MemberDynamicPanel>
|
||||
}
|
||||
|
||||
class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
|
||||
final _dynamicController =
|
||||
Get.put(MemberDynamicPanelController(), tag: Get.arguments['heroTag']);
|
||||
late MemberDynamicPanelController ctr;
|
||||
LoadMoreListSource(this.ctr);
|
||||
|
||||
@override
|
||||
Future<bool> loadData([bool isloadMoreAction = false]) async {
|
||||
bool isSuccess = false;
|
||||
var res = await _dynamicController.getMemberDynamic();
|
||||
var res = await ctr.getMemberDynamic();
|
||||
if (res['status']) {
|
||||
addAll(res['data'].items);
|
||||
}
|
||||
if (res['data'].hasMore) {
|
||||
isSuccess = true;
|
||||
} else {
|
||||
isSuccess = false;
|
||||
}
|
||||
try {
|
||||
if (res['data'].hasMore) {
|
||||
isSuccess = true;
|
||||
} else {
|
||||
isSuccess = false;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return isSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/member/archive/view.dart';
|
||||
import 'package:pilipala/pages/member/dynamic/index.dart';
|
||||
import 'package:pilipala/pages/member/index.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
import 'widgets/profile.dart';
|
||||
|
||||
@ -20,21 +21,26 @@ class MemberPage extends StatefulWidget {
|
||||
|
||||
class _MemberPageState extends State<MemberPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final MemberController _memberController = Get.put(MemberController());
|
||||
late String heroTag;
|
||||
late MemberController _memberController;
|
||||
Future? _futureBuilderFuture;
|
||||
final ScrollController _extendNestCtr = ScrollController();
|
||||
late TabController _tabController;
|
||||
final StreamController<bool> appbarStream = StreamController<bool>();
|
||||
late int mid;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid);
|
||||
_memberController = Get.put(MemberController(), tag: heroTag);
|
||||
_tabController = TabController(length: 3, vsync: this, initialIndex: 2);
|
||||
_futureBuilderFuture = _memberController.getInfo();
|
||||
_extendNestCtr.addListener(
|
||||
() {
|
||||
double offset = _extendNestCtr.position.pixels;
|
||||
if (offset > 250) {
|
||||
if (offset > 230) {
|
||||
appbarStream.add(true);
|
||||
} else {
|
||||
appbarStream.add(false);
|
||||
@ -63,7 +69,7 @@ class _MemberPageState extends State<MemberPage>
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
expandedHeight: 320,
|
||||
expandedHeight: 290,
|
||||
titleSpacing: 0,
|
||||
title: StreamBuilder(
|
||||
stream: appbarStream.stream,
|
||||
@ -77,11 +83,13 @@ class _MemberPageState extends State<MemberPage>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 35,
|
||||
height: 35,
|
||||
type: 'avatar',
|
||||
src: _memberController.face ?? '',
|
||||
Obx(
|
||||
() => NetworkImgLayer(
|
||||
width: 35,
|
||||
height: 35,
|
||||
type: 'avatar',
|
||||
src: _memberController.face.value,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Obx(
|
||||
@ -102,40 +110,83 @@ class _MemberPageState extends State<MemberPage>
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed(
|
||||
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
if (_memberController.ownerMid !=
|
||||
_memberController.mid) ...[
|
||||
PopupMenuItem(
|
||||
onTap: () => _memberController.blockUser(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.block, size: 19),
|
||||
const SizedBox(width: 10),
|
||||
Text(_memberController.attribute.value != 128
|
||||
? '加入黑名单'
|
||||
: '移除黑名单'),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
PopupMenuItem(
|
||||
onTap: () => _memberController.shareUser(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.share_outlined, size: 19),
|
||||
const SizedBox(width: 10),
|
||||
Text(_memberController.ownerMid !=
|
||||
_memberController.mid
|
||||
? '分享UP主'
|
||||
: '分享我的主页'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Stack(
|
||||
children: [
|
||||
if (_memberController.face != null)
|
||||
Positioned.fill(
|
||||
bottom: 10,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
image: NetworkImage(_memberController.face!),
|
||||
alignment: Alignment.topCenter,
|
||||
isAntiAlias: true,
|
||||
),
|
||||
),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.44),
|
||||
Theme.of(context).colorScheme.background,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: const [0.0, 0.46],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => _memberController.face.value != ''
|
||||
? Positioned.fill(
|
||||
bottom: 10,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
image: NetworkImage(
|
||||
_memberController.face.value),
|
||||
alignment: Alignment.topCenter,
|
||||
isAntiAlias: true,
|
||||
),
|
||||
),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.44),
|
||||
Theme.of(context).colorScheme.background,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: const [0.0, 0.46],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@ -145,159 +196,7 @@ class _MemberPageState extends State<MemberPage>
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18),
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done) {
|
||||
Map data = snapshot.data!;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
profile(_memberController),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
_memberController
|
||||
.memberInfo.value.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.copyWith(
|
||||
fontWeight:
|
||||
FontWeight.bold),
|
||||
)),
|
||||
const SizedBox(width: 2),
|
||||
if (_memberController
|
||||
.memberInfo.value.sex ==
|
||||
'女')
|
||||
const Icon(
|
||||
FontAwesomeIcons.venus,
|
||||
size: 14,
|
||||
color: Colors.pink,
|
||||
),
|
||||
if (_memberController
|
||||
.memberInfo.value.sex ==
|
||||
'男')
|
||||
const Icon(
|
||||
FontAwesomeIcons.mars,
|
||||
size: 14,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
|
||||
height: 11,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (_memberController.memberInfo
|
||||
.value.vip!.status ==
|
||||
1 &&
|
||||
_memberController.memberInfo
|
||||
.value.vip!.label![
|
||||
'img_label_uri_hans'] !=
|
||||
'') ...[
|
||||
Image.network(
|
||||
_memberController.memberInfo
|
||||
.value.vip!.label![
|
||||
'img_label_uri_hans'],
|
||||
height: 20,
|
||||
),
|
||||
] else if (_memberController
|
||||
.memberInfo
|
||||
.value
|
||||
.vip!
|
||||
.status ==
|
||||
1 &&
|
||||
_memberController.memberInfo
|
||||
.value.vip!.label![
|
||||
'img_label_uri_hans_static'] !=
|
||||
'') ...[
|
||||
Image.network(
|
||||
_memberController.memberInfo
|
||||
.value.vip!.label![
|
||||
'img_label_uri_hans_static'],
|
||||
height: 20,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
if (_memberController.memberInfo.value
|
||||
.official!['title'] !=
|
||||
'') ...[
|
||||
const SizedBox(height: 6),
|
||||
Text.rich(
|
||||
maxLines: 2,
|
||||
TextSpan(
|
||||
text: _memberController
|
||||
.memberInfo
|
||||
.value
|
||||
.official!['role'] ==
|
||||
1
|
||||
? '个人认证:'
|
||||
: '企业认证:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: _memberController
|
||||
.memberInfo
|
||||
.value
|
||||
.official!['title'],
|
||||
),
|
||||
],
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
if (_memberController
|
||||
.memberInfo.value.sign !=
|
||||
'')
|
||||
SelectableRegion(
|
||||
magnifierConfiguration:
|
||||
const TextMagnifierConfiguration(),
|
||||
focusNode: FocusNode(),
|
||||
selectionControls:
|
||||
MaterialTextSelectionControls(),
|
||||
child: Text(
|
||||
_memberController
|
||||
.memberInfo.value.sign!,
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return profile(_memberController,
|
||||
loadingStatus: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
profileWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -322,10 +221,10 @@ class _MemberPageState extends State<MemberPage>
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: const [
|
||||
Text('主页'),
|
||||
MemberDynamicPanel(),
|
||||
ArchivePanel(),
|
||||
children: [
|
||||
const Text('主页'),
|
||||
MemberDynamicPanel(mid: mid),
|
||||
ArchivePanel(mid: mid),
|
||||
],
|
||||
))
|
||||
],
|
||||
@ -333,4 +232,143 @@ class _MemberPageState extends State<MemberPage>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget profileWidget() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18),
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data!;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
profile(_memberController),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
_memberController.memberInfo.value.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
const SizedBox(width: 2),
|
||||
if (_memberController.memberInfo.value.sex == '女')
|
||||
const Icon(
|
||||
FontAwesomeIcons.venus,
|
||||
size: 14,
|
||||
color: Colors.pink,
|
||||
),
|
||||
if (_memberController.memberInfo.value.sex == '男')
|
||||
const Icon(
|
||||
FontAwesomeIcons.mars,
|
||||
size: 14,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
|
||||
height: 11,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (_memberController
|
||||
.memberInfo.value.vip!.status ==
|
||||
1 &&
|
||||
_memberController.memberInfo.value.vip!
|
||||
.label!['img_label_uri_hans'] !=
|
||||
'') ...[
|
||||
Image.network(
|
||||
_memberController.memberInfo.value.vip!
|
||||
.label!['img_label_uri_hans'],
|
||||
height: 20,
|
||||
),
|
||||
] else if (_memberController
|
||||
.memberInfo.value.vip!.status ==
|
||||
1 &&
|
||||
_memberController.memberInfo.value.vip!
|
||||
.label!['img_label_uri_hans_static'] !=
|
||||
'') ...[
|
||||
Image.network(
|
||||
_memberController.memberInfo.value.vip!
|
||||
.label!['img_label_uri_hans_static'],
|
||||
height: 20,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
if (_memberController
|
||||
.memberInfo.value.official!['title'] !=
|
||||
'') ...[
|
||||
const SizedBox(height: 6),
|
||||
Text.rich(
|
||||
maxLines: 2,
|
||||
TextSpan(
|
||||
text: _memberController
|
||||
.memberInfo.value.official!['role'] ==
|
||||
1
|
||||
? '个人认证:'
|
||||
: '企业认证:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: _memberController
|
||||
.memberInfo.value.official!['title'],
|
||||
),
|
||||
],
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
if (_memberController.memberInfo.value.sign != '')
|
||||
SelectableText(
|
||||
_memberController.memberInfo.value.sign!,
|
||||
maxLines: _memberController
|
||||
.memberInfo.value.official!['title'] !=
|
||||
''
|
||||
? 1
|
||||
: 2,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: SelectableText(_memberController
|
||||
.memberInfo.value.sign!),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return profile(_memberController, loadingStatus: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,62 +15,63 @@ Widget profile(ctr, {loadingStatus = false}) {
|
||||
child: Row(
|
||||
children: [
|
||||
Hero(
|
||||
tag: ctr.heroTag!,
|
||||
child: Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 90,
|
||||
height: 90,
|
||||
type: 'avatar',
|
||||
src: !loadingStatus ? memberInfo.face : ctr.face,
|
||||
),
|
||||
if (!loadingStatus &&
|
||||
memberInfo.liveRoom != null &&
|
||||
memberInfo.liveRoom!.liveStatus == 1)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 14,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
LiveItemModel liveItem = LiveItemModel.fromJson({
|
||||
'title': memberInfo.liveRoom!.title,
|
||||
'uname': memberInfo.name,
|
||||
'face': memberInfo.face,
|
||||
'roomid': memberInfo.liveRoom!.roomId,
|
||||
'watched_show': memberInfo.liveRoom!.watchedShow,
|
||||
});
|
||||
Get.toNamed(
|
||||
'/liveRoom?roomid=${memberInfo.liveRoom!.roomId}',
|
||||
arguments: {'liveItem': liveItem},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Image.asset(
|
||||
'assets/images/live.gif',
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
' 直播中',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize),
|
||||
)
|
||||
]),
|
||||
tag: ctr.heroTag!,
|
||||
child: Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 90,
|
||||
height: 90,
|
||||
type: 'avatar',
|
||||
src: !loadingStatus ? memberInfo.face : ctr.face.value,
|
||||
),
|
||||
if (!loadingStatus &&
|
||||
memberInfo.liveRoom != null &&
|
||||
memberInfo.liveRoom!.liveStatus == 1)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 14,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
LiveItemModel liveItem = LiveItemModel.fromJson({
|
||||
'title': memberInfo.liveRoom!.title,
|
||||
'uname': memberInfo.name,
|
||||
'face': memberInfo.face,
|
||||
'roomid': memberInfo.liveRoom!.roomId,
|
||||
'watched_show': memberInfo.liveRoom!.watchedShow,
|
||||
});
|
||||
Get.toNamed(
|
||||
'/liveRoom?roomid=${memberInfo.liveRoom!.roomId}',
|
||||
arguments: {'liveItem': liveItem},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Image.asset(
|
||||
'assets/images/live.gif',
|
||||
height: 10,
|
||||
),
|
||||
Text(
|
||||
' 直播中',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize),
|
||||
)
|
||||
]),
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -122,12 +123,14 @@ Widget profile(ctr, {loadingStatus = false}) {
|
||||
: '-',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text('粉丝',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize))
|
||||
Text(
|
||||
'粉丝',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -152,34 +155,41 @@ Widget profile(ctr, {loadingStatus = false}) {
|
||||
if (ctr.ownerMid != ctr.mid) ...[
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => ctr.actionRelationMod(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 42, right: 42),
|
||||
foregroundColor:
|
||||
!loadingStatus && memberInfo.isFollowed!
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: !loadingStatus &&
|
||||
memberInfo.isFollowed!
|
||||
? Theme.of(context).colorScheme.onInverseSurface
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, // 设置按钮背景色
|
||||
Obx(
|
||||
() => Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => ctr.actionRelationMod(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: ctr.attribute.value == -1
|
||||
? Colors.transparent
|
||||
: ctr.attribute.value != 0
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
backgroundColor: ctr.attribute.value != 0
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, // 设置按钮背景色
|
||||
),
|
||||
child: Obx(() => Text(ctr.attributeText.value)),
|
||||
),
|
||||
),
|
||||
child: Text(!loadingStatus && memberInfo.isFollowed!
|
||||
? '取关'
|
||||
: '关注'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 42, right: 42),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
child: const Text('发消息'),
|
||||
),
|
||||
child: const Text('发消息'),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
90
lib/pages/member_search/controller.dart
Normal file
90
lib/pages/member_search/controller.dart
Normal file
@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/models/member/archive.dart';
|
||||
|
||||
class MemberSearchController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Rx<TextEditingController> controller = TextEditingController().obs;
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs;
|
||||
String hintText = '搜索';
|
||||
RxString loadingStatus = 'init'.obs;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
bool hasRequest = false;
|
||||
late int mid;
|
||||
RxString uname = ''.obs;
|
||||
int archivePn = 1;
|
||||
int archiveCount = 0;
|
||||
RxList<VListItemModel> archiveList = <VListItemModel>[].obs;
|
||||
int dynamic_pn = 1;
|
||||
RxList<VListItemModel> dynamicList = <VListItemModel>[].obs;
|
||||
|
||||
int ps = 30;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(Get.parameters['mid']!);
|
||||
uname.value = Get.parameters['uname']!;
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
void onClear() {
|
||||
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
|
||||
controller.value.clear();
|
||||
searchKeyWord.value = '';
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
void onChange(value) {
|
||||
searchKeyWord.value = value;
|
||||
}
|
||||
|
||||
// 提交搜索内容
|
||||
void submit() {
|
||||
loadingStatus.value = 'loading';
|
||||
if (hasRequest) {
|
||||
archivePn = 1;
|
||||
searchArchives();
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索视频
|
||||
Future searchArchives({type = 'init'}) async {
|
||||
if (type == 'onLoad' && loadingText.value == '没有更多了') {
|
||||
return;
|
||||
}
|
||||
var res = await MemberHttp.memberArchive(
|
||||
mid: mid,
|
||||
pn: archivePn,
|
||||
keyword: controller.value.text,
|
||||
order: 'pubdate',
|
||||
);
|
||||
if (res['status']) {
|
||||
if (type == 'init' || archivePn == 1) {
|
||||
archiveList.value = res['data'].list.vlist;
|
||||
} else {
|
||||
archiveList.addAll(res['data'].list.vlist);
|
||||
}
|
||||
archiveCount = res['data'].page['count'];
|
||||
if (archiveList.length == archiveCount) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
archivePn += 1;
|
||||
hasRequest = true;
|
||||
}
|
||||
// loadingStatus.value = 'finish';
|
||||
return res;
|
||||
}
|
||||
|
||||
// 搜索动态
|
||||
Future searchDynamic() async {}
|
||||
|
||||
//
|
||||
onLoad() {
|
||||
searchArchives(type: 'onLoad');
|
||||
}
|
||||
}
|
||||
4
lib/pages/member_search/index.dart
Normal file
4
lib/pages/member_search/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library member_search;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
195
lib/pages/member_search/view.dart
Normal file
195
lib/pages/member_search/view.dart
Normal file
@ -0,0 +1,195 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class MemberSearchPage extends StatefulWidget {
|
||||
const MemberSearchPage({super.key});
|
||||
|
||||
@override
|
||||
State<MemberSearchPage> createState() => _MemberSearchPageState();
|
||||
}
|
||||
|
||||
class _MemberSearchPageState extends State<MemberSearchPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final MemberSearchController _memberSearchCtr =
|
||||
Get.put(MemberSearchController());
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = _memberSearchCtr.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
|
||||
_memberSearchCtr.onLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
// _tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// _tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _memberSearchCtr.submit(),
|
||||
icon: const Icon(CupertinoIcons.search, size: 22)),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: Obx(
|
||||
() => TextField(
|
||||
autofocus: true,
|
||||
focusNode: _memberSearchCtr.searchFocusNode,
|
||||
controller: _memberSearchCtr.controller.value,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (value) => _memberSearchCtr.onChange(value),
|
||||
decoration: InputDecoration(
|
||||
hintText: _memberSearchCtr.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 22,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
onPressed: () => _memberSearchCtr.onClear(),
|
||||
),
|
||||
),
|
||||
onSubmitted: (String value) => _memberSearchCtr.submit(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => Column(
|
||||
children: _memberSearchCtr.loadingStatus.value == 'init'
|
||||
? [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('搜索「${_memberSearchCtr.uname.value}」的动态、视频'),
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
// TabBar(
|
||||
// controller: _tabController,
|
||||
// tabs: const [
|
||||
// Tab(text: "视频"),
|
||||
// Tab(text: "动态"),
|
||||
// ],
|
||||
// ),
|
||||
Expanded(
|
||||
child:
|
||||
// TabBarView(
|
||||
// controller: _tabController,
|
||||
// children: [
|
||||
FutureBuilder(
|
||||
future: _memberSearchCtr.searchArchives(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => _memberSearchCtr.archiveList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount:
|
||||
_memberSearchCtr.archiveList.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
_memberSearchCtr
|
||||
.archiveList.length) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_memberSearchCtr
|
||||
.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return VideoCardH(
|
||||
videoItem: _memberSearchCtr
|
||||
.archiveList[index]);
|
||||
}
|
||||
},
|
||||
)
|
||||
: _memberSearchCtr.loadingStatus.value ==
|
||||
'loading'
|
||||
? ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
NoData(),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// ],
|
||||
// ),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
@ -40,6 +41,7 @@ class MineController extends GetxController {
|
||||
'pageTitle': '登录bilibili',
|
||||
},
|
||||
);
|
||||
// Get.toNamed('/loginPage');
|
||||
} else {
|
||||
int mid = userInfo.value.mid!;
|
||||
String face = userInfo.value.face!;
|
||||
@ -111,4 +113,20 @@ class MineController extends GetxController {
|
||||
}
|
||||
Get.forceAppUpdate();
|
||||
}
|
||||
|
||||
pushFollow() {
|
||||
if (!userLogin.value) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
Get.toNamed('/follow?mid=${userInfo.value.mid}');
|
||||
}
|
||||
|
||||
pushFans() {
|
||||
if (!userLogin.value) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
Get.toNamed('/fan?mid=${userInfo.value.mid}');
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +85,9 @@ class _MinePageState extends State<MinePage> {
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
if (snapshot.data['status']) {
|
||||
return Obx(
|
||||
() => userInfoBuild(mineController, context));
|
||||
@ -261,7 +264,7 @@ class _MinePageState extends State<MinePage> {
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Get.toNamed('/follow'),
|
||||
onTap: () => _mineController.pushFollow(),
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -291,7 +294,7 @@ class _MinePageState extends State<MinePage> {
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Get.toNamed('/fan'),
|
||||
onTap: () => _mineController.pushFans(),
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -17,17 +18,6 @@ class PreviewController extends GetxController {
|
||||
bool photos = true;
|
||||
String currentImgUrl = '';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments != null) {
|
||||
initialPage.value = Get.arguments['initialPage']!;
|
||||
currentPage.value = Get.arguments['initialPage']! + 1;
|
||||
imgList.value = Get.arguments['imgList'];
|
||||
currentImgUrl = imgList[initialPage.value];
|
||||
}
|
||||
}
|
||||
|
||||
requestPermission() async {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.storage,
|
||||
@ -40,10 +30,11 @@ class PreviewController extends GetxController {
|
||||
|
||||
// 图片分享
|
||||
void onShareImg() async {
|
||||
requestPermission();
|
||||
SmartDialog.showLoading();
|
||||
var response = await Dio().get(imgList[initialPage.value],
|
||||
options: Options(responseType: ResponseType.bytes));
|
||||
final temp = await getTemporaryDirectory();
|
||||
SmartDialog.dismiss();
|
||||
String imgName =
|
||||
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
|
||||
var path = '${temp.path}/$imgName';
|
||||
|
||||
@ -15,7 +15,13 @@ import 'package:status_bar_control/status_bar_control.dart';
|
||||
typedef DoubleClickAnimationListener = void Function();
|
||||
|
||||
class ImagePreview extends StatefulWidget {
|
||||
const ImagePreview({Key? key}) : super(key: key);
|
||||
final int? initialPage;
|
||||
final List<String>? imgList;
|
||||
const ImagePreview({
|
||||
Key? key,
|
||||
this.initialPage,
|
||||
this.imgList,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ImagePreviewState createState() => _ImagePreviewState();
|
||||
@ -34,6 +40,11 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_previewController.initialPage.value = widget.initialPage!;
|
||||
_previewController.currentPage.value = widget.initialPage! + 1;
|
||||
_previewController.imgList.value = widget.imgList!;
|
||||
_previewController.currentImgUrl = widget.imgList![widget.initialPage!];
|
||||
// animationController = AnimationController(
|
||||
// vsync: this, duration: const Duration(milliseconds: 400));
|
||||
setStatusBar();
|
||||
@ -42,9 +53,8 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
}
|
||||
|
||||
onOpenMenu() {
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@ -55,7 +65,7 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
ListTile(
|
||||
onTap: () {
|
||||
_previewController.onShareImg();
|
||||
SmartDialog.dismiss();
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('分享', style: TextStyle(fontSize: 14)),
|
||||
@ -65,8 +75,8 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: _previewController.currentImgUrl))
|
||||
.then((value) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('已复制到粘贴板');
|
||||
SmartDialog.dismiss();
|
||||
}).catchError((err) {
|
||||
SmartDialog.showNotify(
|
||||
msg: err.toString(),
|
||||
@ -79,6 +89,7 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(_previewController.currentImgUrl);
|
||||
},
|
||||
dense: true,
|
||||
@ -93,13 +104,21 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
|
||||
// 设置状态栏图标透明
|
||||
setStatusBar() async {
|
||||
await StatusBarControl.setHidden(true, animation: StatusBarAnimation.SLIDE);
|
||||
if (Platform.isIOS) {
|
||||
await StatusBarControl.setHidden(true,
|
||||
animation: StatusBarAnimation.SLIDE);
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
await StatusBarControl.setColor(Colors.transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// animationController.dispose();
|
||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
|
||||
try {
|
||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
|
||||
} catch (_) {}
|
||||
_doubleClickAnimationController.dispose();
|
||||
clearGestureDetailsCache();
|
||||
super.dispose();
|
||||
@ -129,109 +148,105 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
disabled: _dismissDisabled,
|
||||
isFullScreen: true,
|
||||
child: Hero(
|
||||
tag: _previewController
|
||||
.imgList[_previewController.initialPage.value],
|
||||
child: GestureDetector(
|
||||
onLongPress: () => onOpenMenu(),
|
||||
child: ExtendedImageGesturePageView.builder(
|
||||
controller: ExtendedPageController(
|
||||
initialPage: _previewController.initialPage.value,
|
||||
pageSpacing: 0,
|
||||
),
|
||||
onPageChanged: (int index) =>
|
||||
_previewController.onChange(index),
|
||||
canScrollPage: (GestureDetails? gestureDetails) =>
|
||||
gestureDetails!.totalScale! <= 1.0,
|
||||
preloadPagesCount: 2,
|
||||
itemCount: _previewController.imgList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ExtendedImage.network(
|
||||
_previewController.imgList[index],
|
||||
fit: BoxFit.contain,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
onDoubleTap: (ExtendedImageGestureState state) {
|
||||
final Offset? pointerDownPosition =
|
||||
state.pointerDownPosition;
|
||||
final double? begin = state.gestureDetails!.totalScale;
|
||||
double end;
|
||||
|
||||
//remove old
|
||||
_doubleClickAnimation
|
||||
?.removeListener(_doubleClickAnimationListener);
|
||||
|
||||
//stop pre
|
||||
_doubleClickAnimationController.stop();
|
||||
|
||||
//reset to use
|
||||
_doubleClickAnimationController.reset();
|
||||
|
||||
if (begin == doubleTapScales[0]) {
|
||||
setState(() {
|
||||
_dismissDisabled = true;
|
||||
});
|
||||
end = doubleTapScales[1];
|
||||
} else {
|
||||
setState(() {
|
||||
_dismissDisabled = false;
|
||||
});
|
||||
end = doubleTapScales[0];
|
||||
}
|
||||
|
||||
_doubleClickAnimationListener = () {
|
||||
state.handleDoubleTap(
|
||||
scale: _doubleClickAnimation!.value,
|
||||
doubleTapPosition: pointerDownPosition);
|
||||
};
|
||||
_doubleClickAnimation = _doubleClickAnimationController
|
||||
.drive(Tween<double>(begin: begin, end: end));
|
||||
|
||||
_doubleClickAnimation!
|
||||
.addListener(_doubleClickAnimationListener);
|
||||
|
||||
_doubleClickAnimationController.forward();
|
||||
},
|
||||
// ignore: body_might_complete_normally_nullable
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress =
|
||||
state.loadingProgress;
|
||||
final double? progress =
|
||||
loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Text('${((progress ?? 0.0) * 100).toInt()}%'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
initGestureConfigHandler: (ExtendedImageState state) {
|
||||
return GestureConfig(
|
||||
inPageView: true,
|
||||
initialScale: 1.0,
|
||||
maxScale: 5.0,
|
||||
animationMaxScale: 6.0,
|
||||
initialAlignment: InitialAlignment.center,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onLongPress: () => onOpenMenu(),
|
||||
child: ExtendedImageGesturePageView.builder(
|
||||
controller: ExtendedPageController(
|
||||
initialPage: _previewController.initialPage.value,
|
||||
pageSpacing: 0,
|
||||
),
|
||||
onPageChanged: (int index) =>
|
||||
_previewController.onChange(index),
|
||||
canScrollPage: (GestureDetails? gestureDetails) =>
|
||||
gestureDetails!.totalScale! <= 1.0,
|
||||
preloadPagesCount: 2,
|
||||
itemCount: widget.imgList!.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ExtendedImage.network(
|
||||
widget.imgList![index],
|
||||
fit: BoxFit.contain,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
onDoubleTap: (ExtendedImageGestureState state) {
|
||||
final Offset? pointerDownPosition =
|
||||
state.pointerDownPosition;
|
||||
final double? begin = state.gestureDetails!.totalScale;
|
||||
double end;
|
||||
|
||||
//remove old
|
||||
_doubleClickAnimation
|
||||
?.removeListener(_doubleClickAnimationListener);
|
||||
|
||||
//stop pre
|
||||
_doubleClickAnimationController.stop();
|
||||
|
||||
//reset to use
|
||||
_doubleClickAnimationController.reset();
|
||||
|
||||
if (begin == doubleTapScales[0]) {
|
||||
setState(() {
|
||||
_dismissDisabled = true;
|
||||
});
|
||||
end = doubleTapScales[1];
|
||||
} else {
|
||||
setState(() {
|
||||
_dismissDisabled = false;
|
||||
});
|
||||
end = doubleTapScales[0];
|
||||
}
|
||||
|
||||
_doubleClickAnimationListener = () {
|
||||
state.handleDoubleTap(
|
||||
scale: _doubleClickAnimation!.value,
|
||||
doubleTapPosition: pointerDownPosition);
|
||||
};
|
||||
_doubleClickAnimation = _doubleClickAnimationController
|
||||
.drive(Tween<double>(begin: begin, end: end));
|
||||
|
||||
_doubleClickAnimation!
|
||||
.addListener(_doubleClickAnimationListener);
|
||||
|
||||
_doubleClickAnimationController.forward();
|
||||
},
|
||||
// ignore: body_might_complete_normally_nullable
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress =
|
||||
state.loadingProgress;
|
||||
final double? progress =
|
||||
loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
// const SizedBox(height: 10.0),
|
||||
// Text('${((progress ?? 0.0) * 100).toInt()}%',),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
initGestureConfigHandler: (ExtendedImageState state) {
|
||||
return GestureConfig(
|
||||
inPageView: true,
|
||||
initialScale: 1.0,
|
||||
maxScale: 5.0,
|
||||
animationMaxScale: 6.0,
|
||||
initialAlignment: InitialAlignment.center,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -241,7 +256,7 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom, top: 20),
|
||||
bottom: MediaQuery.of(context).padding.bottom + 30),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
@ -262,8 +277,7 @@ class _ImagePreviewState extends State<ImagePreview>
|
||||
TextSpan(
|
||||
text: _previewController.currentPage.toString()),
|
||||
const TextSpan(text: ' / '),
|
||||
TextSpan(
|
||||
text: _previewController.imgList.length.toString()),
|
||||
TextSpan(text: widget.imgList!.length.toString()),
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
||||
@ -14,12 +14,13 @@ class RcmdController extends GetxController {
|
||||
Box recVideo = GStrorage.recVideo;
|
||||
Box setting = GStrorage.setting;
|
||||
RxInt crossAxisCount = 2.obs;
|
||||
late bool enableSaveLastData;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
crossAxisCount.value =
|
||||
setting.get(SettingBoxKey.enableSingleRow, defaultValue: false) ? 1 : 2;
|
||||
setting.get(SettingBoxKey.customRows, defaultValue: 2);
|
||||
if (recVideo.get('cacheList') != null &&
|
||||
recVideo.get('cacheList').isNotEmpty) {
|
||||
List<RecVideoItemAppModel> list = [];
|
||||
@ -28,6 +29,8 @@ class RcmdController extends GetxController {
|
||||
}
|
||||
videoList.value = list;
|
||||
}
|
||||
enableSaveLastData =
|
||||
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
|
||||
}
|
||||
|
||||
// 获取推荐
|
||||
@ -49,7 +52,11 @@ class RcmdController extends GetxController {
|
||||
videoList.value = res['data'];
|
||||
}
|
||||
} else if (type == 'onRefresh') {
|
||||
videoList.insertAll(0, res['data']);
|
||||
if (enableSaveLastData) {
|
||||
videoList.insertAll(0, res['data']);
|
||||
} else {
|
||||
videoList.value = res['data'];
|
||||
}
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(res['data']);
|
||||
}
|
||||
|
||||
@ -77,7 +77,8 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return await _rcmdController.onRefresh();
|
||||
await _rcmdController.onRefresh();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _rcmdController.scrollController,
|
||||
@ -124,7 +125,7 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
},
|
||||
),
|
||||
),
|
||||
const LoadingMore()
|
||||
LoadingMore(ctr: _rcmdController)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -190,7 +191,8 @@ class _RcmdPageState extends State<RcmdPage>
|
||||
}
|
||||
|
||||
class LoadingMore extends StatelessWidget {
|
||||
const LoadingMore({super.key});
|
||||
dynamic ctr;
|
||||
LoadingMore({super.key, this.ctr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -198,11 +200,18 @@ class LoadingMore extends StatelessWidget {
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).padding.bottom + 80,
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'加载中...',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline, fontSize: 13),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (ctr != null) {
|
||||
ctr!.onLoad();
|
||||
}
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
'加载更多 👇',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline, fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -12,7 +12,7 @@ class SSearchController extends GetxController {
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs;
|
||||
Rx<TextEditingController> controller = TextEditingController().obs;
|
||||
RxList<HotSearchItem> hotSearchList = [HotSearchItem()].obs;
|
||||
RxList<HotSearchItem> hotSearchList = <HotSearchItem>[].obs;
|
||||
Box histiryWord = GStrorage.historyword;
|
||||
List historyCacheList = [];
|
||||
RxList historyList = [].obs;
|
||||
@ -27,7 +27,9 @@ class SSearchController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
searchDefault();
|
||||
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
|
||||
searchDefault();
|
||||
}
|
||||
// 其他页面跳转过来
|
||||
if (Get.parameters.keys.isNotEmpty) {
|
||||
if (Get.parameters['keyword'] != null) {
|
||||
@ -83,7 +85,9 @@ class SSearchController extends GetxController {
|
||||
// 获取热搜关键词
|
||||
Future queryHotSearchList() async {
|
||||
var result = await SearchHttp.hotSearchList();
|
||||
hotSearchList.value = result['data'].list;
|
||||
if (result['status']) {
|
||||
hotSearchList.value = result['data'].list;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -101,7 +105,9 @@ class SSearchController extends GetxController {
|
||||
Future querySearchSuggest(String value) async {
|
||||
var result = await SearchHttp.searchSuggest(term: value);
|
||||
if (result['status']) {
|
||||
searchSuggestList.value = result['data'].tag;
|
||||
if (result['data'] is SearchSuggestModel) {
|
||||
searchSuggestList.value = result['data'].tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +117,13 @@ class SSearchController extends GetxController {
|
||||
submit();
|
||||
}
|
||||
|
||||
onLongSelect(word) {
|
||||
int index = historyList.indexOf(word);
|
||||
historyList.value = historyList.removeAt(index);
|
||||
historyList.refresh();
|
||||
histiryWord.put('cacheList', historyList);
|
||||
}
|
||||
|
||||
onClearHis() {
|
||||
historyList.value = [];
|
||||
historyCacheList = [];
|
||||
|
||||
@ -227,6 +227,9 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
@ -296,20 +299,24 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
),
|
||||
),
|
||||
// if (_searchController.historyList.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (int i = 0; i < _searchController.historyList.length; i++)
|
||||
SearchText(
|
||||
searchText: _searchController.historyList[i],
|
||||
searchTextIdx: i,
|
||||
onSelect: (value) => _searchController.onSelect(value),
|
||||
)
|
||||
],
|
||||
),
|
||||
Obx(() => Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (int i = 0;
|
||||
i < _searchController.historyList.length;
|
||||
i++)
|
||||
SearchText(
|
||||
searchText: _searchController.historyList[i],
|
||||
searchTextIdx: i,
|
||||
onSelect: (value) => _searchController.onSelect(value),
|
||||
onLongSelect: (value) =>
|
||||
_searchController.onLongSelect(value),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -4,8 +4,14 @@ class SearchText extends StatelessWidget {
|
||||
final String? searchText;
|
||||
final Function? onSelect;
|
||||
final int? searchTextIdx;
|
||||
const SearchText(
|
||||
{super.key, this.searchText, this.onSelect, this.searchTextIdx});
|
||||
final Function? onLongSelect;
|
||||
const SearchText({
|
||||
super.key,
|
||||
this.searchText,
|
||||
this.onSelect,
|
||||
this.searchTextIdx,
|
||||
this.onLongSelect,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -18,6 +24,9 @@ class SearchText extends StatelessWidget {
|
||||
onTap: () {
|
||||
onSelect!(searchText);
|
||||
},
|
||||
onLongPress: () {
|
||||
onLongSelect!(searchText);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding:
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'widgets/article_panel.dart';
|
||||
import 'widgets/live_panel.dart';
|
||||
import 'widgets/media_bangumi_panel.dart';
|
||||
import 'widgets/user_panel.dart';
|
||||
@ -90,6 +91,8 @@ class _SearchPanelState extends State<SearchPanel>
|
||||
return searchUserPanel(context, ctr, list);
|
||||
case SearchType.live_room:
|
||||
return searchLivePanel(context, ctr, list);
|
||||
case SearchType.article:
|
||||
return searchArticlePanel(context, ctr, list);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
107
lib/pages/searchPanel/widgets/article_panel.dart
Normal file
107
lib/pages/searchPanel/widgets/article_panel.dart
Normal file
@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
Widget searchArticlePanel(BuildContext context, ctr, list) {
|
||||
TextStyle textStyle = TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline);
|
||||
return ListView.builder(
|
||||
controller: ctr!.scrollController,
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Get.toNamed('/htmlRender', parameters: {
|
||||
'url': 'www.bilibili.com/read/cv${list[index].id}',
|
||||
'title': list[index].subTitle,
|
||||
'id': 'cv${list[index].id}',
|
||||
'dynamicType': 'read'
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double width = (boxConstraints.maxWidth -
|
||||
StyleString.cardSpace *
|
||||
6 /
|
||||
MediaQuery.of(context).textScaleFactor) /
|
||||
2;
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 88),
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (list[index].imageUrls != null &&
|
||||
list[index].imageUrls.isNotEmpty)
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return NetworkImgLayer(
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
src: list[index].imageUrls.first,
|
||||
);
|
||||
}),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
maxLines: 2,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
for (var i in list[index].title) ...[
|
||||
TextSpan(
|
||||
text: i['text'],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
color: i['type'] == 'em'
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
Utils.dateFormat(list[index].pubTime,
|
||||
formatType: 'detail'),
|
||||
style: textStyle),
|
||||
Row(
|
||||
children: [
|
||||
Text('${list[index].view}浏览', style: textStyle),
|
||||
Text(' • ', style: textStyle),
|
||||
Text('${list[index].reply}评论', style: textStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -16,8 +16,12 @@ class ExtraSetting extends StatefulWidget {
|
||||
|
||||
class _ExtraSettingState extends State<ExtraSetting> {
|
||||
Box setting = GStrorage.setting;
|
||||
static Box localCache = GStrorage.localCache;
|
||||
late dynamic defaultReplySort;
|
||||
late dynamic defaultDynamicType;
|
||||
late dynamic enableSystemProxy;
|
||||
late String defaultSystemProxyHost;
|
||||
late String defaultSystemProxyPort;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -28,6 +32,86 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
// 优先展示全部动态 all
|
||||
defaultDynamicType =
|
||||
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
|
||||
enableSystemProxy =
|
||||
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
|
||||
defaultSystemProxyHost =
|
||||
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
|
||||
defaultSystemProxyPort =
|
||||
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
|
||||
}
|
||||
|
||||
// 设置代理
|
||||
void twoFADialog() {
|
||||
var systemProxyHost = '';
|
||||
var systemProxyPort = '';
|
||||
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('设置代理'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: defaultSystemProxyHost != ''
|
||||
? defaultSystemProxyHost
|
||||
: '请输入Host,使用 . 分割',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
hintText: defaultSystemProxyHost,
|
||||
),
|
||||
onChanged: (e) {
|
||||
systemProxyHost = e;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: defaultSystemProxyPort != ''
|
||||
? defaultSystemProxyPort
|
||||
: '请输入Port',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
hintText: defaultSystemProxyPort,
|
||||
),
|
||||
onChanged: (e) {
|
||||
systemProxyPort = e;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
localCache.put(LocalCacheKey.systemProxyHost, systemProxyHost);
|
||||
localCache.put(LocalCacheKey.systemProxyPort, systemProxyPort);
|
||||
SmartDialog.dismiss();
|
||||
// Request.dio;
|
||||
},
|
||||
child: const Text('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -55,6 +139,18 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
defaultVal: true,
|
||||
callFn: (val) => {SmartDialog.showToast('下次启动时生效')},
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '搜索默认词',
|
||||
subTitle: '是否展示搜索框默认词',
|
||||
setKey: SettingBoxKey.enableSearchWord,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '推荐动态',
|
||||
subTitle: '是否在推荐内容中展示动态',
|
||||
setKey: SettingBoxKey.enableRcmdDynamic,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '快速收藏',
|
||||
subTitle: '点按收藏至默认,长按选择文件夹',
|
||||
@ -67,6 +163,12 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
setKey: SettingBoxKey.enableWordRe,
|
||||
defaultVal: false,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '首页推荐刷新',
|
||||
subTitle: '下拉刷新时保留上次内容',
|
||||
setKey: SettingBoxKey.enableSaveLastData,
|
||||
defaultVal: false,
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
title: Text('评论展示', style: titleStyle),
|
||||
@ -117,6 +219,33 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
enableFeedback: true,
|
||||
onTap: () => twoFADialog(),
|
||||
title: Text('设置代理', style: titleStyle),
|
||||
subtitle: Text('设置代理 host:port', style: subTitleStyle),
|
||||
trailing: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Switch(
|
||||
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.isNotEmpty &&
|
||||
states.first == MaterialState.selected) {
|
||||
return const Icon(Icons.done);
|
||||
}
|
||||
return null; // All other states will use the default thumbIcon.
|
||||
}),
|
||||
value: enableSystemProxy,
|
||||
onChanged: (val) {
|
||||
setting.put(
|
||||
SettingBoxKey.enableSystemProxy, !enableSystemProxy);
|
||||
setState(() {
|
||||
enableSystemProxy = !enableSystemProxy;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '检查更新',
|
||||
subTitle: '每次启动时检查是否需要更新',
|
||||
|
||||
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class SetDiaplayMode extends StatefulWidget {
|
||||
const SetDiaplayMode({super.key});
|
||||
@ -14,6 +16,7 @@ class _SetDiaplayModeState extends State<SetDiaplayMode> {
|
||||
List<DisplayMode> modes = <DisplayMode>[];
|
||||
DisplayMode? active;
|
||||
DisplayMode? preferred;
|
||||
Box setting = GStrorage.setting;
|
||||
|
||||
final ValueNotifier<int> page = ValueNotifier<int>(0);
|
||||
late final PageController controller = PageController()
|
||||
@ -29,24 +32,36 @@ class _SetDiaplayModeState extends State<SetDiaplayMode> {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有的mode
|
||||
Future<void> fetchAll() async {
|
||||
preferred = await FlutterDisplayMode.preferred;
|
||||
active = await FlutterDisplayMode.active;
|
||||
// GStorage().setDisplayModeType(preferred!);
|
||||
await setting.put(SettingBoxKey.displayMode, preferred.toString());
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// 初始化mode/手动设置
|
||||
Future<void> init() async {
|
||||
try {
|
||||
modes = await FlutterDisplayMode.supported;
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
}
|
||||
// var res = await GStorage().getDisplayModeType();
|
||||
// preferred = modes.toList().firstWhere((el) => el == res);
|
||||
var res = await getDisplayModeType(modes);
|
||||
|
||||
preferred = modes.toList().firstWhere((el) => el == res);
|
||||
FlutterDisplayMode.setPreferredMode(preferred!);
|
||||
}
|
||||
|
||||
Future<DisplayMode> getDisplayModeType(modes) async {
|
||||
var value = setting.get(SettingBoxKey.displayMode);
|
||||
DisplayMode f = DisplayMode.auto;
|
||||
if (value != null) {
|
||||
f = modes.firstWhere((e) => e.toString() == value);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
304
lib/pages/setting/pages/play_speed_set.dart
Normal file
304
lib/pages/setting/pages/play_speed_set.dart
Normal file
@ -0,0 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_speed.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class PlaySpeedPage extends StatefulWidget {
|
||||
const PlaySpeedPage({super.key});
|
||||
|
||||
@override
|
||||
State<PlaySpeedPage> createState() => _PlaySpeedPageState();
|
||||
}
|
||||
|
||||
class _PlaySpeedPageState extends State<PlaySpeedPage> {
|
||||
Box videoStorage = GStrorage.video;
|
||||
late double playSpeedDefault;
|
||||
late double longPressSpeedDefault;
|
||||
late List customSpeedsList;
|
||||
List<Map<dynamic, dynamic>> sheetMenu = [
|
||||
{
|
||||
'id': 1,
|
||||
'title': '设置为默认倍速',
|
||||
'leading': const Icon(
|
||||
Icons.speed,
|
||||
size: 21,
|
||||
),
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'title': '设置为默认长按倍速',
|
||||
'leading': const Icon(
|
||||
Icons.speed_sharp,
|
||||
size: 21,
|
||||
),
|
||||
},
|
||||
{
|
||||
'id': -1,
|
||||
'title': '删除该项',
|
||||
'leading': const Icon(
|
||||
Icons.delete_outline,
|
||||
size: 21,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 默认倍速
|
||||
playSpeedDefault =
|
||||
videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
|
||||
// 默认长按倍速
|
||||
longPressSpeedDefault =
|
||||
videoStorage.get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0);
|
||||
// 自定义倍速
|
||||
customSpeedsList =
|
||||
videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []);
|
||||
}
|
||||
|
||||
// 添加自定义倍速
|
||||
void onAddSpeed() {
|
||||
double customSpeed = 1.0;
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('添加倍速'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// const Text('输入你想要的视频倍速,例如:1.0'),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: '自定义倍速',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
onChanged: (e) {
|
||||
customSpeed = double.parse(e);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
customSpeedsList.add(customSpeed);
|
||||
await videoStorage.put(
|
||||
VideoBoxKey.customSpeedsList, customSpeedsList);
|
||||
setState(() {});
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
child: const Text('确认添加'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 设定倍速弹窗
|
||||
void showBottomSheet(type, i) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
//重要
|
||||
itemCount: sheetMenu.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
menuAction(type, i, sheetMenu[index]['id']);
|
||||
},
|
||||
minLeadingWidth: 0,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
leading: sheetMenu[index]['leading'],
|
||||
title: Text(
|
||||
sheetMenu[index]['title'],
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
void menuAction(type, index, id) async {
|
||||
double chooseSpeed = 1.0;
|
||||
if (type == 'system' && id == -1) {
|
||||
SmartDialog.showToast('系统预设倍速不支持删除');
|
||||
return;
|
||||
}
|
||||
// 获取当前选中的倍速值
|
||||
if (type == 'system') {
|
||||
chooseSpeed = PlaySpeed.values[index].value;
|
||||
} else {
|
||||
chooseSpeed = customSpeedsList[index];
|
||||
}
|
||||
// 设置
|
||||
if (id == 1) {
|
||||
// 设置默认倍速
|
||||
playSpeedDefault = chooseSpeed;
|
||||
videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault);
|
||||
} else if (id == 2) {
|
||||
// 设置默认长按倍速
|
||||
longPressSpeedDefault = chooseSpeed;
|
||||
videoStorage.put(
|
||||
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
|
||||
} else if (id == -1) {
|
||||
if (customSpeedsList[index] == playSpeedDefault) {
|
||||
playSpeedDefault = 1.0;
|
||||
videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault);
|
||||
}
|
||||
if (customSpeedsList[index] == longPressSpeedDefault) {
|
||||
longPressSpeedDefault = 2.0;
|
||||
videoStorage.put(
|
||||
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
|
||||
}
|
||||
customSpeedsList.removeAt(index);
|
||||
await videoStorage.put(VideoBoxKey.customSpeedsList, customSpeedsList);
|
||||
}
|
||||
setState(() {});
|
||||
SmartDialog.showToast('操作成功');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'倍速设置',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 14, right: 14, top: 6, bottom: 0),
|
||||
child: Text(
|
||||
'点击下方按钮设置默认(长按)倍速',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
title: const Text('默认倍速'),
|
||||
subtitle: Text(playSpeedDefault.toString()),
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
title: const Text('默认长按倍速'),
|
||||
subtitle: Text(longPressSpeedDefault.toString()),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 14,
|
||||
right: 14,
|
||||
bottom: 10,
|
||||
top: 20,
|
||||
),
|
||||
child: Text(
|
||||
'系统预设倍速',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 18,
|
||||
right: 18,
|
||||
bottom: 30,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (var i in PlaySpeed.values) ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () => showBottomSheet('system', i.index),
|
||||
child: Text(i.description),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 14,
|
||||
right: 14,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'自定义倍速',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
TextButton(
|
||||
onPressed: () => onAddSpeed(),
|
||||
child: const Text('添加'),
|
||||
)
|
||||
],
|
||||
)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 18,
|
||||
right: 18,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 40,
|
||||
),
|
||||
child: customSpeedsList.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (int i = 0; i < customSpeedsList.length; i++) ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () => showBottomSheet('custom', i),
|
||||
child: Text(customSpeedsList[i].toString()),
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
: SizedBox(
|
||||
height: 80,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'未添加',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/switch_item.dart';
|
||||
@ -36,6 +38,14 @@ class _PlaySettingState extends State<PlaySetting> {
|
||||
defaultValue: BtmProgresBehavior.values.first.code);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
// 重新验证媒体通知后台播放设置
|
||||
videoPlayerServiceHandler.revalidateSetting();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
|
||||
@ -54,12 +64,42 @@ class _PlaySettingState extends State<PlaySetting> {
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
dense: false,
|
||||
onTap: () => Get.toNamed('/playSpeedSet'),
|
||||
title: Text('倍速设置', style: titleStyle),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 17),
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '开启1080P',
|
||||
subTitle: '免登录查看1080P视频',
|
||||
setKey: SettingBoxKey.p1080,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: 'CDN优化',
|
||||
subTitle: '使用优质CDN线路',
|
||||
setKey: SettingBoxKey.enableCDN,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '自动播放',
|
||||
subTitle: '进入详情页自动播放',
|
||||
setKey: SettingBoxKey.autoPlayEnable,
|
||||
defaultVal: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '后台播放',
|
||||
subTitle: '进入后台时继续播放',
|
||||
setKey: SettingBoxKey.enableBackgroundPlay,
|
||||
defaultVal: false,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '自动PiP播放',
|
||||
subTitle: 'app切换至后台时画中画播放',
|
||||
setKey: SettingBoxKey.autoPiP,
|
||||
defaultVal: false,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '自动全屏',
|
||||
subTitle: '视频开始播放时进入全屏',
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/common/theme_type.dart';
|
||||
@ -22,12 +21,14 @@ class _StyleSettingState extends State<StyleSetting> {
|
||||
Box setting = GStrorage.setting;
|
||||
late int picQuality;
|
||||
late ThemeType _tempThemeValue;
|
||||
late dynamic defaultCustomRows;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
|
||||
_tempThemeValue = settingController.themeType.value;
|
||||
defaultCustomRows = setting.get(SettingBoxKey.customRows, defaultValue: 2);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -76,12 +77,43 @@ class _StyleSettingState extends State<StyleSetting> {
|
||||
setKey: SettingBoxKey.iosTransition,
|
||||
defaultVal: false,
|
||||
),
|
||||
SetSwitchItem(
|
||||
title: '首页单列',
|
||||
subTitle: '每行展示一个内容卡片',
|
||||
setKey: SettingBoxKey.enableSingleRow,
|
||||
defaultVal: false,
|
||||
callFn: (val) => {SmartDialog.showToast('下次启动时生效')},
|
||||
const SetSwitchItem(
|
||||
title: 'MD3样式底栏',
|
||||
subTitle: '符合Material You设计规范的底栏',
|
||||
setKey: SettingBoxKey.enableMYBar,
|
||||
defaultVal: true,
|
||||
),
|
||||
// SetSwitchItem(
|
||||
// title: '首页单列',
|
||||
// subTitle: '每行展示一个内容卡片',
|
||||
// setKey: SettingBoxKey.enableSingleRow,
|
||||
// defaultVal: false,
|
||||
// callFn: (val) => {SmartDialog.showToast('下次启动时生效')},
|
||||
// ),
|
||||
ListTile(
|
||||
dense: false,
|
||||
title: Text('自定义列数', style: titleStyle),
|
||||
subtitle: Text(
|
||||
'当前列数',
|
||||
style: subTitleStyle,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
initialValue: defaultCustomRows,
|
||||
icon: const Icon(Icons.more_vert_outlined, size: 22),
|
||||
onSelected: (item) {
|
||||
defaultCustomRows = item;
|
||||
setting.put(SettingBoxKey.customRows, item);
|
||||
setState(() {});
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
for (var i in [1, 2, 3, 4, 5]) ...[
|
||||
PopupMenuItem(
|
||||
value: i,
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -14,13 +16,16 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/utils/video_utils.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'widgets/header_control.dart';
|
||||
|
||||
class VideoDetailController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
/// 路由传参
|
||||
String bvid = Get.parameters['bvid']!;
|
||||
int cid = int.parse(Get.parameters['cid']!);
|
||||
RxInt cid = int.parse(Get.parameters['cid']!).obs;
|
||||
RxInt danmakuCid = 0.obs;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
// 视频详情
|
||||
@ -76,6 +81,13 @@ class VideoDetailController extends GetxController
|
||||
bool enableHeart = true;
|
||||
var userInfo;
|
||||
late bool isFirstTime = true;
|
||||
Floating? floating;
|
||||
late PreferredSizeWidget headerControl;
|
||||
|
||||
late bool enableCDN;
|
||||
late int? cacheVideoQa;
|
||||
late String cacheDecode;
|
||||
late int cacheAudioQa;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -103,7 +115,26 @@ class VideoDetailController extends GetxController
|
||||
localCache.get(LocalCacheKey.historyPause) == true) {
|
||||
enableHeart = false;
|
||||
}
|
||||
danmakuCid.value = cid;
|
||||
danmakuCid.value = cid.value;
|
||||
|
||||
///
|
||||
if (Platform.isAndroid) {
|
||||
floating = Floating();
|
||||
}
|
||||
headerControl = HeaderControl(
|
||||
controller: plPlayerController,
|
||||
videoDetailCtr: this,
|
||||
floating: floating,
|
||||
);
|
||||
// CDN优化
|
||||
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
|
||||
// 预设的画质
|
||||
cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa);
|
||||
// 预设的解码格式
|
||||
cacheDecode = setting.get(SettingBoxKey.defaultDecode,
|
||||
defaultValue: VideoDecodeFormats.values.last.code);
|
||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
}
|
||||
|
||||
showReplyReplyPanel() {
|
||||
@ -167,7 +198,13 @@ class VideoDetailController extends GetxController
|
||||
playerInit();
|
||||
}
|
||||
|
||||
Future playerInit({video, audio, seekToTime, duration}) async {
|
||||
Future playerInit({
|
||||
video,
|
||||
audio,
|
||||
seekToTime,
|
||||
duration,
|
||||
bool autoplay = true,
|
||||
}) async {
|
||||
/// 设置/恢复 屏幕亮度
|
||||
if (brightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(brightness!);
|
||||
@ -193,36 +230,35 @@ class VideoDetailController extends GetxController
|
||||
direction: (firstVideo.width! - firstVideo.height!) > 0
|
||||
? 'horizontal'
|
||||
: 'vertical',
|
||||
// 默认1倍速
|
||||
speed: 1.0,
|
||||
bvid: bvid,
|
||||
cid: cid,
|
||||
cid: cid.value,
|
||||
enableHeart: enableHeart,
|
||||
isFirstTime: isFirstTime,
|
||||
autoplay: autoplay,
|
||||
);
|
||||
|
||||
/// 开启自动全屏时,在player初始化完成后立即传入headerControl
|
||||
plPlayerController.headerControl = headerControl;
|
||||
}
|
||||
|
||||
// 视频链接
|
||||
Future queryVideoUrl() async {
|
||||
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
||||
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
|
||||
if (result['status']) {
|
||||
data = result['data'];
|
||||
|
||||
List<VideoItem> allVideosList = data.dash!.video!;
|
||||
|
||||
try {
|
||||
// 当前可播放的最高质量视频
|
||||
int currentHighVideoQa = allVideosList.first.quality!.code;
|
||||
// 使用预设的画质 | 当前可用的最高质量
|
||||
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
|
||||
defaultValue: currentHighVideoQa);
|
||||
// 预设的画质为null,则当前可用的最高质量
|
||||
cacheVideoQa ??= currentHighVideoQa;
|
||||
int resVideoQa = currentHighVideoQa;
|
||||
if (cacheVideoQa <= currentHighVideoQa) {
|
||||
if (cacheVideoQa! <= currentHighVideoQa) {
|
||||
// 如果预设的画质低于当前最高
|
||||
List<int> numbers = data.acceptQuality!
|
||||
.where((e) => e <= currentHighVideoQa)
|
||||
.toList();
|
||||
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
|
||||
resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers);
|
||||
}
|
||||
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
|
||||
|
||||
@ -236,9 +272,7 @@ class VideoDetailController extends GetxController
|
||||
List supportDecodeFormats =
|
||||
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
|
||||
// 默认从设置中取AVC
|
||||
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
|
||||
SettingBoxKey.defaultDecode,
|
||||
defaultValue: VideoDecodeFormats.values.last.code))!;
|
||||
currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!;
|
||||
try {
|
||||
// 当前视频没有对应格式返回第一个
|
||||
bool flag = false;
|
||||
@ -250,8 +284,8 @@ class VideoDetailController extends GetxController
|
||||
currentDecodeFormats = flag
|
||||
? currentDecodeFormats
|
||||
: VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast('DecodeFormats error: $err');
|
||||
}
|
||||
|
||||
/// 取出符合当前解码格式的videoItem
|
||||
@ -261,9 +295,11 @@ class VideoDetailController extends GetxController
|
||||
} catch (_) {
|
||||
firstVideo = videosList.first;
|
||||
}
|
||||
videoUrl = firstVideo.baseUrl!;
|
||||
videoUrl = enableCDN
|
||||
? VideoUtils.getCdnUrl(firstVideo)
|
||||
: (firstVideo.backupUrl ?? firstVideo.baseUrl!);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
SmartDialog.showToast('firstVideo error: $err');
|
||||
}
|
||||
|
||||
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
|
||||
@ -271,9 +307,6 @@ class VideoDetailController extends GetxController
|
||||
List<AudioItem> audiosList = data.dash!.audio!;
|
||||
|
||||
try {
|
||||
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
|
||||
if (data.dash!.dolby?.audio?.isNotEmpty == true) {
|
||||
// 杜比
|
||||
audiosList.insert(0, data.dash!.dolby!.audio!.first);
|
||||
@ -286,14 +319,23 @@ class VideoDetailController extends GetxController
|
||||
|
||||
if (audiosList.isNotEmpty) {
|
||||
List<int> numbers = audiosList.map((map) => map.id!).toList();
|
||||
int closestNumber = Utils.findClosestNumber(resultAudioQa, numbers);
|
||||
int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
|
||||
if (!numbers.contains(cacheAudioQa) &&
|
||||
numbers.any((e) => e > cacheAudioQa)) {
|
||||
closestNumber = 30280;
|
||||
}
|
||||
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
|
||||
} else {
|
||||
firstAudio = AudioItem();
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} catch (err) {
|
||||
firstAudio = audiosList.isNotEmpty ? audiosList.first : AudioItem();
|
||||
SmartDialog.showToast('firstAudio error: $err');
|
||||
}
|
||||
|
||||
audioUrl = firstAudio!.baseUrl ?? '';
|
||||
audioUrl = enableCDN
|
||||
? VideoUtils.getCdnUrl(firstAudio)
|
||||
: (firstAudio.backupUrl ?? firstAudio.baseUrl!);
|
||||
//
|
||||
if (firstAudio.id != null) {
|
||||
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
|
||||
|
||||
@ -8,14 +8,18 @@ import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/models/video/ai.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import 'widgets/group_panel.dart';
|
||||
|
||||
class VideoIntroController extends GetxController {
|
||||
// 视频bvid
|
||||
String bvid = Get.parameters['bvid']!;
|
||||
@ -58,11 +62,16 @@ class VideoIntroController extends GetxController {
|
||||
RxString total = '1'.obs;
|
||||
Timer? timer;
|
||||
bool isPaused = false;
|
||||
String heroTag = '';
|
||||
late ModelResult modelResult;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
try {
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
} catch (_) {}
|
||||
if (Get.arguments.isNotEmpty) {
|
||||
if (Get.arguments.containsKey('videoItem')) {
|
||||
preRender = true;
|
||||
@ -102,9 +111,10 @@ class VideoIntroController extends GetxController {
|
||||
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
|
||||
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
|
||||
}
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||
.tabs
|
||||
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];
|
||||
// Get.find<VideoDetailController>(tag: heroTag).tabs.value = [
|
||||
// '简介',
|
||||
// '评论 ${result['data']!.stat!.reply}'
|
||||
// ];
|
||||
// 获取到粉丝数再返回
|
||||
await queryUserStat();
|
||||
}
|
||||
@ -330,7 +340,8 @@ class VideoIntroController extends GetxController {
|
||||
|
||||
// 分享视频
|
||||
Future actionShareVideo() async {
|
||||
var result = await Share.share('${HttpString.baseUrl}/video/$bvid')
|
||||
var result = await Share.share(
|
||||
'${videoDetail.value.title} - ${HttpString.baseUrl}/video/$bvid')
|
||||
.whenComplete(() {});
|
||||
return result;
|
||||
}
|
||||
@ -424,6 +435,20 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
followStatus['attribute'] = actionStatus;
|
||||
followStatus.refresh();
|
||||
if (actionStatus == 2) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('关注成功'),
|
||||
duration: const Duration(seconds: 2),
|
||||
action: SnackBarAction(
|
||||
label: '设置分组',
|
||||
onPressed: setFollowGroup,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
@ -439,16 +464,16 @@ class VideoIntroController extends GetxController {
|
||||
Future changeSeasonOrbangu(bvid, cid, aid) async {
|
||||
// 重新获取视频资源
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
videoDetailCtr.bvid = bvid;
|
||||
videoDetailCtr.cid = cid;
|
||||
videoDetailCtr.cid.value = cid;
|
||||
videoDetailCtr.danmakuCid.value = cid;
|
||||
videoDetailCtr.queryVideoUrl();
|
||||
// 重新请求评论
|
||||
try {
|
||||
/// 未渲染回复组件时可能异常
|
||||
VideoReplyController videoReplyCtr =
|
||||
Get.find<VideoReplyController>(tag: Get.arguments['heroTag']);
|
||||
Get.find<VideoReplyController>(tag: heroTag);
|
||||
videoReplyCtr.aid = aid;
|
||||
videoReplyCtr.queryReplyList(type: 'init');
|
||||
} catch (_) {}
|
||||
@ -485,4 +510,74 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 列表循环或者顺序播放时,自动播放下一个
|
||||
void nextPlay() {
|
||||
late List episodes;
|
||||
bool isPages = false;
|
||||
if (videoDetail.value.ugcSeason != null) {
|
||||
UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
|
||||
List<SectionItem> sections = ugcSeason.sections!;
|
||||
episodes = [];
|
||||
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
episodes.addAll(episodesList);
|
||||
}
|
||||
} else if (videoDetail.value.pages != null) {
|
||||
isPages = true;
|
||||
List<Part> pages = videoDetail.value.pages!;
|
||||
episodes = [];
|
||||
episodes.addAll(pages);
|
||||
}
|
||||
|
||||
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
|
||||
int nextIndex = currentIndex + 1;
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
|
||||
|
||||
// 列表循环
|
||||
if (nextIndex >= episodes.length) {
|
||||
if (platRepeat == PlayRepeat.listCycle) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
if (platRepeat == PlayRepeat.listOrder) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
int cid = episodes[nextIndex].cid!;
|
||||
String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
|
||||
int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
|
||||
changeSeasonOrbangu(rBvid, cid, rAid);
|
||||
}
|
||||
|
||||
// 设置关注分组
|
||||
void setFollowGroup() {
|
||||
Get.bottomSheet(
|
||||
GroupPanel(mid: videoDetail.value.owner!.mid!),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
// ai总结
|
||||
Future aiConclusion() async {
|
||||
SmartDialog.showLoading(msg: '正在生产ai总结');
|
||||
var res = await VideoHttp.aiConclusion(
|
||||
bvid: bvid,
|
||||
cid: lastPlayCid.value,
|
||||
upMid: videoDetail.value.owner!.mid!,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (res['data'].modelResult.resultType == 0) {
|
||||
SmartDialog.showToast('该视频不支持ai总结');
|
||||
}
|
||||
if (res['data'].modelResult.resultType == 2 ||
|
||||
res['data'].modelResult.resultType == 1) {
|
||||
modelResult = res['data'].modelResult;
|
||||
}
|
||||
}
|
||||
SmartDialog.dismiss();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -199,6 +201,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
|
||||
// 视频介绍
|
||||
showIntroDetail() {
|
||||
if (loadingStatus) {
|
||||
return;
|
||||
}
|
||||
feedBack();
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
@ -223,6 +228,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
arguments: {'face': face, 'heroTag': memberHeroTag});
|
||||
}
|
||||
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
enableDrag: true,
|
||||
builder: (BuildContext context) {
|
||||
return AiDetail(modelResult: videoIntroController.modelResult);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ThemeData t = Theme.of(context);
|
||||
@ -238,103 +254,100 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
!loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding:
|
||||
MaterialStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor:
|
||||
MaterialStateProperty.resolveWith((states) {
|
||||
return t.highlightColor.withOpacity(0.2);
|
||||
}),
|
||||
),
|
||||
onPressed: showIntroDetail,
|
||||
icon: Icon(
|
||||
Icons.more_horiz,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
!loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.view
|
||||
: videoItem['stat'].view,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.danmaku
|
||||
: videoItem['stat'].danmaku,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.pubdate
|
||||
: videoItem['pubdate'],
|
||||
formatType: 'detail'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (videoIntroController.isShowOnlineTotal)
|
||||
Obx(
|
||||
() => Text(
|
||||
'${videoIntroController.total.value}人在看',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 7, bottom: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.view
|
||||
: videoItem['stat'].view,
|
||||
size: 'medium',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.danmaku
|
||||
: videoItem['stat'].danmaku,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.pubdate
|
||||
: videoItem['pubdate'],
|
||||
formatType: 'detail'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (videoIntroController.isShowOnlineTotal)
|
||||
Obx(
|
||||
() => Text(
|
||||
'${videoIntroController.total.value}人在看',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 10,
|
||||
top: 6,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var res = await videoIntroController.aiConclusion();
|
||||
if (res['status']) {
|
||||
if (res['data'].modelResult.resultType == 2 ||
|
||||
res['data'].modelResult.resultType == 1) {
|
||||
showAiBottomSheet();
|
||||
}
|
||||
}
|
||||
},
|
||||
child:
|
||||
Image.asset('assets/images/ai.png', height: 22),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
// 点赞收藏转发 布局样式1
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: actionRow(
|
||||
context,
|
||||
videoIntroController,
|
||||
videoDetailCtr,
|
||||
),
|
||||
),
|
||||
// SingleChildScrollView(
|
||||
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||
// scrollDirection: Axis.horizontal,
|
||||
// child: actionRow(
|
||||
// context,
|
||||
// videoIntroController,
|
||||
// videoDetailCtr,
|
||||
// ),
|
||||
// ),
|
||||
// 点赞收藏转发 布局样式2
|
||||
// actionGrid(context, videoIntroController),
|
||||
actionGrid(context, videoIntroController),
|
||||
// 合集
|
||||
if (!loadingStatus &&
|
||||
widget.videoDetail!.ugcSeason != null) ...[
|
||||
@ -452,7 +465,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
Widget actionGrid(BuildContext context, videoIntroController) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 10),
|
||||
margin: const EdgeInsets.only(top: 6, bottom: 4),
|
||||
height: constraints.maxWidth / 5 * 0.8,
|
||||
child: GridView.count(
|
||||
primary: false,
|
||||
@ -471,12 +484,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
? widget.videoDetail!.stat!.like!.toString()
|
||||
: '-'),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.clock),
|
||||
onTap: () => videoIntroController.actionShareVideo(),
|
||||
selectStatus: false,
|
||||
loadingStatus: loadingStatus,
|
||||
text: '稍后再看'),
|
||||
// ActionItem(
|
||||
// icon: const Icon(FontAwesomeIcons.clock),
|
||||
// onTap: () => videoIntroController.actionShareVideo(),
|
||||
// selectStatus: false,
|
||||
// loadingStatus: loadingStatus,
|
||||
// text: '稍后再看'),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.b),
|
||||
@ -492,22 +505,28 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.star),
|
||||
selectIcon: const Icon(FontAwesomeIcons.solidStar),
|
||||
// onTap: () => videoIntroController.actionFavVideo(),
|
||||
onTap: () => showFavBottomSheet(),
|
||||
onLongPress: () => showFavBottomSheet(type: 'longPress'),
|
||||
selectStatus: videoIntroController.hasFav.value,
|
||||
loadingStatus: loadingStatus,
|
||||
text: !loadingStatus
|
||||
? widget.videoDetail!.stat!.favorite!.toString()
|
||||
: '-'),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.comment),
|
||||
onTap: () => videoDetailCtr.tabCtr.animateTo(1),
|
||||
selectStatus: false,
|
||||
loadingStatus: loadingStatus,
|
||||
text: !loadingStatus
|
||||
? widget.videoDetail!.stat!.reply!.toString()
|
||||
: '评论'),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.shareFromSquare),
|
||||
onTap: () => videoIntroController.actionShareVideo(),
|
||||
selectStatus: false,
|
||||
loadingStatus: loadingStatus,
|
||||
text: !loadingStatus
|
||||
? widget.videoDetail!.stat!.share!.toString()
|
||||
: '-'),
|
||||
text: '分享'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@ class ActionItem extends StatelessWidget {
|
||||
final Icon? icon;
|
||||
final Icon? selectIcon;
|
||||
final Function? onTap;
|
||||
final Function? onLongPress;
|
||||
final bool? loadingStatus;
|
||||
final String? text;
|
||||
final bool selectStatus;
|
||||
@ -15,6 +16,7 @@ class ActionItem extends StatelessWidget {
|
||||
this.icon,
|
||||
this.selectIcon,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.loadingStatus,
|
||||
this.text,
|
||||
this.selectStatus = false,
|
||||
@ -27,6 +29,9 @@ class ActionItem extends StatelessWidget {
|
||||
feedBack(),
|
||||
onTap!(),
|
||||
},
|
||||
onLongPress: () => {
|
||||
if (onLongPress != null) {onLongPress!()}
|
||||
},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
156
lib/pages/video/detail/introduction/widgets/group_panel.dart
Normal file
156
lib/pages/video/detail/introduction/widgets/group_panel.dart
Normal file
@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/models/member/tags.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class GroupPanel extends StatefulWidget {
|
||||
final int? mid;
|
||||
const GroupPanel({super.key, this.mid});
|
||||
|
||||
@override
|
||||
State<GroupPanel> createState() => _GroupPanelState();
|
||||
}
|
||||
|
||||
class _GroupPanelState extends State<GroupPanel> {
|
||||
Box localCache = GStrorage.localCache;
|
||||
late double sheetHeight;
|
||||
late Future _futureBuilderFuture;
|
||||
late List<MemberTagItemModel> tagsList;
|
||||
bool showDefault = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
_futureBuilderFuture = MemberHttp.followUpTags();
|
||||
}
|
||||
|
||||
void onSave() async {
|
||||
feedBack();
|
||||
// 是否有选中的 有选中的带id,没选使用默认0
|
||||
bool anyHasChecked = tagsList.any((e) => e.checked == true);
|
||||
late String tagids;
|
||||
if (anyHasChecked) {
|
||||
List checkedList = tagsList.where((e) => e.checked == true).toList();
|
||||
List<int> tagidList = checkedList.map<int>((e) => e.tagid).toList();
|
||||
tagids = tagidList.join(',');
|
||||
} else {
|
||||
tagids = '0';
|
||||
}
|
||||
// 保存
|
||||
var res = await MemberHttp.addUsers(widget.mid, tagids);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
if (res['status']) {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: sheetHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close_outlined)),
|
||||
title:
|
||||
Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
tagsList = data['data'];
|
||||
return ListView.builder(
|
||||
itemCount: data['data'].length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
data['data'][index].checked =
|
||||
!data['data'][index].checked;
|
||||
showDefault =
|
||||
!data['data'].any((e) => e.checked == true);
|
||||
setState(() {});
|
||||
},
|
||||
dense: true,
|
||||
leading: const Icon(Icons.group_outlined),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(data['data'][index].name),
|
||||
subtitle: data['data'][index].tip != ''
|
||||
? Text(data['data'][index].tip)
|
||||
: null,
|
||||
trailing: Transform.scale(
|
||||
scale: 0.9,
|
||||
child: Checkbox(
|
||||
value: data['data'][index].checked,
|
||||
onChanged: (bool? checkValue) {
|
||||
data['data'][index].checked = checkValue;
|
||||
showDefault = !data['data']
|
||||
.any((e) => e.checked == true);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const Text('请求中');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).disabledColor.withOpacity(0.08),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 12,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => onSave(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 30, right: 30),
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary, // 设置按钮背景色
|
||||
),
|
||||
child: Text(showDefault ? '保存至默认分组' : '保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
@ -129,7 +130,50 @@ class IntroDetail extends StatelessWidget {
|
||||
final currentDesc = descV2[index];
|
||||
switch (currentDesc.type) {
|
||||
case 1:
|
||||
return TextSpan(text: currentDesc.rawText);
|
||||
List<InlineSpan> spanChildren = [];
|
||||
RegExp urlRegExp = RegExp(r'https?://\S+\b');
|
||||
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
|
||||
|
||||
int previousEndIndex = 0;
|
||||
for (Match match in matches) {
|
||||
if (match.start > previousEndIndex) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText
|
||||
.substring(previousEndIndex, match.start)));
|
||||
}
|
||||
spanChildren.add(
|
||||
TextSpan(
|
||||
text: match.group(0),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 处理点击事件
|
||||
try {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': match.group(0)!,
|
||||
'type': 'url',
|
||||
'pageTitle': match.group(0)!,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
previousEndIndex = match.end;
|
||||
}
|
||||
|
||||
if (previousEndIndex < currentDesc.rawText.length) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText.substring(previousEndIndex)));
|
||||
}
|
||||
|
||||
TextSpan result = TextSpan(children: spanChildren);
|
||||
return result;
|
||||
case 2:
|
||||
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
|
||||
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
|
||||
|
||||
@ -17,35 +17,31 @@ class MenuRow extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: [
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'推荐',
|
||||
selectStatus: true,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'弹幕',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '推荐',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'评论列表',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '弹幕',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'播放列表',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '评论列表',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '播放列表',
|
||||
selectStatus: false,
|
||||
),
|
||||
]),
|
||||
@ -99,3 +95,62 @@ class MenuRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionRowLineItem extends StatelessWidget {
|
||||
final bool? selectStatus;
|
||||
final Function? onTap;
|
||||
final bool? loadingStatus;
|
||||
final String? text;
|
||||
|
||||
const ActionRowLineItem(
|
||||
{super.key,
|
||||
this.selectStatus,
|
||||
this.onTap,
|
||||
this.text,
|
||||
this.loadingStatus = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: selectStatus!
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: InkWell(
|
||||
onTap: () => {
|
||||
feedBack(),
|
||||
onTap!(),
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
border: Border.all(
|
||||
color: selectStatus!
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: loadingStatus! ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
text!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: selectStatus!
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
|
||||
class PagesPanel extends StatefulWidget {
|
||||
final List<Part> pages;
|
||||
@ -22,13 +23,23 @@ class PagesPanel extends StatefulWidget {
|
||||
|
||||
class _PagesPanelState extends State<PagesPanel> {
|
||||
late List<Part> episodes;
|
||||
late int cid;
|
||||
late int currentIndex;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late VideoDetailController _videoDetailController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cid = widget.cid!;
|
||||
episodes = widget.pages;
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
_videoDetailController.cid.listen((p0) {
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
});
|
||||
}
|
||||
|
||||
void changeFucCall(item, i) async {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
|
||||
class SeasonPanel extends StatefulWidget {
|
||||
@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget {
|
||||
|
||||
class _SeasonPanelState extends State<SeasonPanel> {
|
||||
late List<EpisodeItem> episodes;
|
||||
late int cid;
|
||||
late int currentIndex;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late VideoDetailController _videoDetailController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cid = widget.cid!;
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
|
||||
/// 根据 cid 找到对应集,找到对应 episodes
|
||||
/// 有多个episodes时,只显示其中一个
|
||||
@ -36,7 +42,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
for (int j = 0; j < episodesList.length; j++) {
|
||||
if (episodesList[j].cid == widget.cid) {
|
||||
if (episodesList[j].cid == cid) {
|
||||
episodes = episodesList;
|
||||
continue;
|
||||
}
|
||||
@ -47,7 +53,12 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
// episodes = widget.ugcSeason.sections!
|
||||
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
|
||||
// .episodes!;
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
_videoDetailController.cid.listen((p0) {
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
currentIndex = episodes.indexWhere((e) => e.cid == cid);
|
||||
});
|
||||
}
|
||||
|
||||
void changeFucCall(item, i) async {
|
||||
@ -57,7 +68,9 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
item.aid,
|
||||
);
|
||||
currentIndex = i;
|
||||
setState(() {});
|
||||
Get.back();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/animated_dialog.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/overlay_pop.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
import './controller.dart';
|
||||
@ -22,6 +23,9 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
|
||||
future: _releatedController.queryRelatedVideo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
if (snapshot.data!['status']) {
|
||||
// 请求成功
|
||||
return SliverList(
|
||||
@ -51,9 +55,7 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
|
||||
}, childCount: snapshot.data['data'].length + 1));
|
||||
} else {
|
||||
// 请求错误
|
||||
return const Center(
|
||||
child: Text('出错了'),
|
||||
);
|
||||
return HttpError(errMsg: '出错了', fn: () {});
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
|
||||
@ -92,12 +92,12 @@ class VideoReplyController extends GetxController {
|
||||
}
|
||||
}
|
||||
replies.insertAll(0, res['data'].topReplies);
|
||||
count.value = res['data'].page.count;
|
||||
replyList.value = replies;
|
||||
} else {
|
||||
replyList.addAll(replies);
|
||||
}
|
||||
}
|
||||
count.value = res['data'].page.count;
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
Future? _futureBuilderFuture;
|
||||
bool _isFabVisible = true;
|
||||
String replyLevel = '1';
|
||||
late String heroTag;
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
@ -46,22 +47,29 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
|
||||
super.initState();
|
||||
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
replyLevel = widget.replyLevel ?? '1';
|
||||
if (replyLevel == '2') {
|
||||
_videoReplyController = Get.put(
|
||||
VideoReplyController(oid, widget.rpid.toString(), replyLevel),
|
||||
tag: widget.rpid.toString());
|
||||
} else {
|
||||
_videoReplyController = Get.put(VideoReplyController(oid, '', replyLevel),
|
||||
tag: Get.arguments['heroTag']);
|
||||
_videoReplyController =
|
||||
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag);
|
||||
}
|
||||
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 300));
|
||||
|
||||
_futureBuilderFuture = _videoReplyController.queryReplyList();
|
||||
|
||||
fabAnimationCtr.forward();
|
||||
scrollListener();
|
||||
}
|
||||
|
||||
void scrollListener() {
|
||||
scrollController = _videoReplyController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
@ -81,7 +89,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
}
|
||||
},
|
||||
);
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
|
||||
void _showFab() {
|
||||
@ -101,7 +108,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
// 展示二级回复
|
||||
void replyReply(replyItem) {
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
if (replyItem != null) {
|
||||
videoDetailCtr.oid = replyItem.oid;
|
||||
videoDetailCtr.fRpid = replyItem.rpid!;
|
||||
@ -112,9 +119,10 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
scrollController.removeListener(() {});
|
||||
fabAnimationCtr.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -128,7 +136,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: _videoReplyController.scrollController,
|
||||
controller: scrollController,
|
||||
key: const PageStorageKey<String>('评论'),
|
||||
slivers: <Widget>[
|
||||
SliverPersistentHeader(
|
||||
@ -187,7 +195,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
|
||||
@ -7,9 +7,11 @@ import 'package:pilipala/common/widgets/badge.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
@ -666,46 +668,71 @@ InlineSpan buildContent(
|
||||
// 匹配 jumpUrl
|
||||
String matchUrl = matchMember;
|
||||
if (content.jumpUrl.isNotEmpty && hasMatchMember) {
|
||||
List urlKeys = content.jumpUrl.keys.toList();
|
||||
List urlKeys = content.jumpUrl.keys.toList().reversed.toList();
|
||||
for (var index = 0; index < urlKeys.length; index++) {
|
||||
var i = urlKeys[index];
|
||||
if (i.contains('?')) {
|
||||
urlKeys[index] = i.replaceAll('?', '\\?');
|
||||
}
|
||||
}
|
||||
matchUrl = matchMember.splitMapJoin(
|
||||
/// RegExp.escape() 转义特殊字符
|
||||
RegExp(RegExp.escape(urlKeys.join("|"))),
|
||||
RegExp(urlKeys.map((key) => key).join("|")),
|
||||
// RegExp('What does the fox say\\?'),
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
String appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||
String appUrlSchema = '';
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||
}
|
||||
// 默认不显示关键词
|
||||
bool enableWordRe =
|
||||
setting.get(SettingBoxKey.enableWordRe, defaultValue: false);
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
String str = Uri.parse(matchStr).pathSegments[0];
|
||||
Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
@ -743,11 +770,14 @@ InlineSpan buildContent(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 跳转到指定位置
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(seconds: Utils.duration(matchStr)),
|
||||
);
|
||||
try {
|
||||
Get.find<VideoDetailController>(
|
||||
tag: Get.arguments['heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(seconds: Utils.duration(matchStr)),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -773,7 +803,7 @@ InlineSpan buildContent(
|
||||
|
||||
// 图片渲染
|
||||
if (content.pictures.isNotEmpty) {
|
||||
List picList = [];
|
||||
List<String> picList = [];
|
||||
int len = content.pictures.length;
|
||||
if (len == 1) {
|
||||
Map pictureItem = content.pictures.first;
|
||||
@ -785,8 +815,13 @@ InlineSpan buildContent(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': 0, 'imgList': picList});
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: 0, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
@ -814,8 +849,13 @@ InlineSpan buildContent(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': i, 'imgList': picList});
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: i, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
src: content.pictures[i]['img_src'],
|
||||
|
||||
@ -26,11 +26,6 @@ class VideoReplyReplyController extends GetxController {
|
||||
currentPage = 0;
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
queryReplyList(type: 'onLoad');
|
||||
}
|
||||
|
||||
Future queryReplyList({type = 'init'}) async {
|
||||
if (type == 'init') {
|
||||
currentPage = 0;
|
||||
@ -49,11 +44,11 @@ class VideoReplyReplyController extends GetxController {
|
||||
if (replyList.length == res['data'].page.count) {
|
||||
noMore.value = '没有更多了';
|
||||
}
|
||||
currentPage++;
|
||||
} else {
|
||||
// 未登录状态replies可能返回null
|
||||
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
|
||||
}
|
||||
currentPage++;
|
||||
if (type == 'init') {
|
||||
// List<ReplyItemModel> replies = res['data'].replies;
|
||||
// 添加置顶回复
|
||||
@ -72,6 +67,10 @@ class VideoReplyReplyController extends GetxController {
|
||||
// res['data'].replies = replies;
|
||||
replyList.value = replies;
|
||||
} else {
|
||||
// 每次回复之后,翻页请求有且只有相同的一条回复数据
|
||||
if (replies.length == 1 && replies.last.rpid == replyList.last.rpid) {
|
||||
return;
|
||||
}
|
||||
replyList.addAll(replies);
|
||||
// res['data'].replies.addAll(replyList);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
@ -54,9 +55,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
if (!_videoReplyReplyController.isLoadingMore) {
|
||||
_videoReplyReplyController.onLoad();
|
||||
}
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_videoReplyReplyController.queryReplyList(type: 'onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/common/widgets/sliver_header.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/pages/bangumi/introduction/index.dart';
|
||||
import 'package:pilipala/pages/danmaku/view.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/related/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/app_bar.dart';
|
||||
import 'widgets/header_control.dart';
|
||||
|
||||
class VideoDetailPage extends StatefulWidget {
|
||||
@ -32,14 +33,14 @@ class VideoDetailPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
with TickerProviderStateMixin, RouteAware {
|
||||
final VideoDetailController videoDetailController =
|
||||
Get.put(VideoDetailController(), tag: Get.arguments['heroTag']);
|
||||
with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
|
||||
late VideoDetailController videoDetailController;
|
||||
PlPlayerController? plPlayerController;
|
||||
final ScrollController _extendNestCtr = ScrollController();
|
||||
late StreamController<double> appbarStream;
|
||||
final VideoIntroController videoIntroController =
|
||||
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
|
||||
late VideoIntroController videoIntroController;
|
||||
late BangumiIntroController bangumiIntroController;
|
||||
late String heroTag;
|
||||
|
||||
PlayerStatus playerStatus = PlayerStatus.playing;
|
||||
double doubleOffset = 0;
|
||||
@ -51,15 +52,39 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
late Future _futureBuilderFuture;
|
||||
// 自动退出全屏
|
||||
late bool autoExitFullcreen;
|
||||
late bool autoPlayEnable;
|
||||
late bool autoPiP;
|
||||
final floating = Floating();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
|
||||
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
|
||||
videoIntroController.videoDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
|
||||
bangumiIntroController.bangumiDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
videoDetailController.cid.listen((p0) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
bangumiIntroController.bangumiDetail.value, p0);
|
||||
});
|
||||
statusBarHeight = localCache.get('statusBarHeight');
|
||||
autoExitFullcreen =
|
||||
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
||||
autoPlayEnable =
|
||||
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
|
||||
autoPiP = setting.get(SettingBoxKey.autoPiP, defaultValue: false);
|
||||
|
||||
videoSourceInit();
|
||||
appbarStreamListen();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
// 获取视频资源,初始化播放器
|
||||
@ -83,15 +108,38 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
void playerListener(PlayerStatus? status) {
|
||||
void playerListener(PlayerStatus? status) async {
|
||||
playerStatus = status!;
|
||||
if (status == PlayerStatus.completed) {
|
||||
// 结束播放退出全屏
|
||||
if (autoExitFullcreen) {
|
||||
plPlayerController!.triggerFullScreen(status: false);
|
||||
}
|
||||
|
||||
/// 顺序播放 列表循环
|
||||
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
|
||||
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
|
||||
if (videoDetailController.videoType == SearchType.video) {
|
||||
videoIntroController.nextPlay();
|
||||
}
|
||||
if (videoDetailController.videoType == SearchType.media_bangumi) {
|
||||
bangumiIntroController.nextPlay();
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个循环
|
||||
if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {
|
||||
plPlayerController!.seekTo(Duration.zero);
|
||||
plPlayerController!.play();
|
||||
}
|
||||
// 播放完展示控制栏
|
||||
plPlayerController!.onLockControl(false);
|
||||
try {
|
||||
PiPStatus currentStatus =
|
||||
await videoDetailController.floating!.pipStatus;
|
||||
if (currentStatus == PiPStatus.disabled) {
|
||||
plPlayerController!.onLockControl(false);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +150,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
plPlayerController!.play();
|
||||
}
|
||||
|
||||
/// 未开启自动播放时触发播放
|
||||
Future<void> handlePlay() async {
|
||||
await videoDetailController.playerInit();
|
||||
plPlayerController = videoDetailController.plPlayerController;
|
||||
@ -111,8 +160,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.dispose();
|
||||
if (plPlayerController != null) {
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.dispose();
|
||||
}
|
||||
if (videoDetailController.floating != null) {
|
||||
videoDetailController.floating!.dispose();
|
||||
}
|
||||
videoPlayerServiceHandler.onVideoDetailDispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
floating.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -123,10 +180,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)) {
|
||||
videoDetailController.brightness = plPlayerController!.brightness.value;
|
||||
}
|
||||
videoDetailController.defaultST = plPlayerController!.position.value;
|
||||
videoIntroController.isPaused = true;
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.pause();
|
||||
if (plPlayerController != null) {
|
||||
videoDetailController.defaultST = plPlayerController!.position.value;
|
||||
videoIntroController.isPaused = true;
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.pause();
|
||||
}
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@ -134,13 +193,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
// 返回当前页面时
|
||||
void didPopNext() async {
|
||||
videoDetailController.isFirstTime = false;
|
||||
videoDetailController.playerInit();
|
||||
bool autoplay = autoPlayEnable;
|
||||
videoDetailController.playerInit(autoplay: autoplay);
|
||||
|
||||
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
|
||||
videoDetailController.autoPlay.value =
|
||||
!videoDetailController.isShowCover.value;
|
||||
videoIntroController.isPaused = false;
|
||||
if (_extendNestCtr.position.pixels == 0) {
|
||||
if (_extendNestCtr.position.pixels == 0 && autoplay) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
plPlayerController!.play();
|
||||
plPlayerController!.seekTo(videoDetailController.defaultST);
|
||||
plPlayerController?.play();
|
||||
}
|
||||
plPlayerController!.addStatusLister(playerListener);
|
||||
plPlayerController?.addStatusLister(playerListener);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@ -151,12 +216,23 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
|
||||
if (lifecycleState == AppLifecycleState.inactive && autoPiP) {
|
||||
floating.enable(
|
||||
aspectRatio: Rational(
|
||||
videoDetailController.data.dash!.video!.first.width!,
|
||||
videoDetailController.data.dash!.video!.first.height!,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
|
||||
final double pinnedHeaderHeight =
|
||||
statusBarHeight + kToolbarHeight + videoHeight;
|
||||
return SafeArea(
|
||||
Widget childWhenDisabled = SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
@ -198,12 +274,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
? const SizedBox()
|
||||
: PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
headerControl: HeaderControl(
|
||||
controller:
|
||||
plPlayerController,
|
||||
videoDetailCtr:
|
||||
videoDetailController,
|
||||
),
|
||||
headerControl:
|
||||
videoDetailController
|
||||
.headerControl,
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
key: Key(
|
||||
@ -336,13 +409,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
];
|
||||
},
|
||||
// pinnedHeaderSliverHeightBuilder: () {
|
||||
// return playerStatus != PlayerStatus.playing
|
||||
// ? statusBarHeight + kToolbarHeight
|
||||
// : pinnedHeaderHeight;
|
||||
// },
|
||||
/// 不收回
|
||||
pinnedHeaderSliverHeightBuilder: () {
|
||||
return playerStatus != PlayerStatus.playing
|
||||
? statusBarHeight + kToolbarHeight
|
||||
: pinnedHeaderHeight;
|
||||
return pinnedHeaderHeight;
|
||||
},
|
||||
onlyOneScrollInBody: true,
|
||||
body: Container(
|
||||
key: Key(heroTag),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
@ -378,8 +456,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
const VideoIntroPanel(),
|
||||
] else if (videoDetailController.videoType ==
|
||||
SearchType.media_bangumi) ...[
|
||||
BangumiIntroPanel(
|
||||
cid: videoDetailController.cid)
|
||||
Obx(() => BangumiIntroPanel(
|
||||
cid: videoDetailController.cid.value)),
|
||||
],
|
||||
// if (videoDetailController.videoType ==
|
||||
// SearchType.video) ...[
|
||||
@ -418,21 +496,61 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 重新进入会刷新
|
||||
// 播放完成/暂停播放
|
||||
StreamBuilder(
|
||||
stream: appbarStream.stream,
|
||||
initialData: 0,
|
||||
builder: ((context, snapshot) {
|
||||
return ScrollAppBar(
|
||||
snapshot.data!.toDouble(),
|
||||
() => continuePlay(),
|
||||
playerStatus,
|
||||
null,
|
||||
);
|
||||
}),
|
||||
)
|
||||
// StreamBuilder(
|
||||
// stream: appbarStream.stream,
|
||||
// initialData: 0,
|
||||
// builder: ((context, snapshot) {
|
||||
// return ScrollAppBar(
|
||||
// snapshot.data!.toDouble(),
|
||||
// () => continuePlay(),
|
||||
// playerStatus,
|
||||
// null,
|
||||
// );
|
||||
// }),
|
||||
// )
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget childWhenEnabled = FutureBuilder(
|
||||
key: Key(heroTag),
|
||||
future: _futureBuilderFuture,
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data['status']) {
|
||||
return Obx(
|
||||
() => !videoDetailController.autoPlay.value
|
||||
? const SizedBox()
|
||||
: PLVideoPlayer(
|
||||
controller: plPlayerController!,
|
||||
headerControl: HeaderControl(
|
||||
controller: plPlayerController,
|
||||
videoDetailCtr: videoDetailController,
|
||||
),
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
key: Key(
|
||||
videoDetailController.danmakuCid.value.toString()),
|
||||
cid: videoDetailController.danmakuCid.value,
|
||||
playerController: plPlayerController!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
return PiPSwitcher(
|
||||
childWhenDisabled: childWhenDisabled,
|
||||
childWhenEnabled: childWhenEnabled,
|
||||
floating: floating,
|
||||
);
|
||||
} else {
|
||||
return childWhenDisabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
236
lib/pages/video/detail/widgets/ai_detail.dart
Normal file
@ -0,0 +1,236 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/video/ai.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
Box localCache = GStrorage.localCache;
|
||||
late double sheetHeight;
|
||||
|
||||
class AiDetail extends StatelessWidget {
|
||||
final ModelResult? modelResult;
|
||||
|
||||
const AiDetail({
|
||||
Key? key,
|
||||
this.modelResult,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
height: sheetHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
height: 35,
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
modelResult!.summary!,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: modelResult!.outline!.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
modelResult!.outline![index].title!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: modelResult!
|
||||
.outline![index].partOutline!.length,
|
||||
itemBuilder: (context, i) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
height: 1.5,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: Utils.tampToSeektime(
|
||||
modelResult!
|
||||
.outline![index]
|
||||
.partOutline![i]
|
||||
.timestamp!),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 跳转到指定位置
|
||||
try {
|
||||
Get.find<VideoDetailController>(
|
||||
tag: Get.arguments[
|
||||
'heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(
|
||||
seconds:
|
||||
Utils.duration(
|
||||
Utils.tampToSeektime(modelResult!
|
||||
.outline![
|
||||
index]
|
||||
.partOutline![
|
||||
i]
|
||||
.timestamp!)
|
||||
.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: modelResult!
|
||||
.outline![index]
|
||||
.partOutline![i]
|
||||
.content!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InlineSpan buildContent(BuildContext context, content) {
|
||||
List descV2 = content.descV2;
|
||||
// type
|
||||
// 1 普通文本
|
||||
// 2 @用户
|
||||
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
|
||||
final currentDesc = descV2[index];
|
||||
switch (currentDesc.type) {
|
||||
case 1:
|
||||
List<InlineSpan> spanChildren = [];
|
||||
RegExp urlRegExp = RegExp(r'https?://\S+\b');
|
||||
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
|
||||
|
||||
int previousEndIndex = 0;
|
||||
for (Match match in matches) {
|
||||
if (match.start > previousEndIndex) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText
|
||||
.substring(previousEndIndex, match.start)));
|
||||
}
|
||||
spanChildren.add(
|
||||
TextSpan(
|
||||
text: match.group(0),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 处理点击事件
|
||||
try {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': match.group(0)!,
|
||||
'type': 'url',
|
||||
'pageTitle': match.group(0)!,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
previousEndIndex = match.end;
|
||||
}
|
||||
|
||||
if (previousEndIndex < currentDesc.rawText.length) {
|
||||
spanChildren.add(TextSpan(
|
||||
text: currentDesc.rawText.substring(previousEndIndex)));
|
||||
}
|
||||
|
||||
TextSpan result = TextSpan(children: spanChildren);
|
||||
return result;
|
||||
case 2:
|
||||
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
|
||||
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
|
||||
return TextSpan(
|
||||
text: '@${currentDesc.rawText}',
|
||||
style: TextStyle(color: colorSchemePrimary),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
Get.toNamed(
|
||||
'/member?mid=${currentDesc.bizId}',
|
||||
arguments: {'face': '', 'heroTag': heroTag},
|
||||
);
|
||||
},
|
||||
);
|
||||
default:
|
||||
return const TextSpan();
|
||||
}
|
||||
});
|
||||
return TextSpan(children: spanChilds);
|
||||
}
|
||||
}
|
||||
@ -48,15 +48,15 @@ class ScrollAppBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.share,
|
||||
size: 20,
|
||||
)),
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(
|
||||
// Icons.share,
|
||||
// size: 20,
|
||||
// )),
|
||||
// const SizedBox(width: 12)
|
||||
// ],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,18 +1,29 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
import 'package:pilipala/models/video/play/url.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
|
||||
final PlPlayerController? controller;
|
||||
final VideoDetailController? videoDetailCtr;
|
||||
final Floating? floating;
|
||||
const HeaderControl({
|
||||
this.controller,
|
||||
this.videoDetailCtr,
|
||||
this.floating,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -29,11 +40,16 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
|
||||
TextStyle titleStyle = const TextStyle(fontSize: 14);
|
||||
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
|
||||
Box localCache = GStrorage.localCache;
|
||||
Box videoStorage = GStrorage.video;
|
||||
late List speedsList;
|
||||
double buttonSpace = 8;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
videoInfo = widget.videoDetailCtr!.data;
|
||||
speedsList = widget.controller!.speedsList;
|
||||
}
|
||||
|
||||
/// 设置面板
|
||||
@ -45,7 +61,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
builder: (_) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 400,
|
||||
height: 440,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
@ -73,7 +89,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
@ -138,17 +153,17 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
|
||||
style: subTitleStyle),
|
||||
),
|
||||
// ListTile(
|
||||
// onTap: () {},
|
||||
// dense: true,
|
||||
// enabled: false,
|
||||
// leading: const Icon(Icons.play_circle_outline, size: 20),
|
||||
// title: Text('播放设置', style: titleStyle),
|
||||
// ),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
onTap: () => {Get.back(), showSetRepeat()},
|
||||
dense: true,
|
||||
leading: const Icon(Icons.repeat, size: 20),
|
||||
title: Text('播放顺序', style: titleStyle),
|
||||
subtitle: Text(widget.controller!.playRepeat.description,
|
||||
style: subTitleStyle),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => {Get.back(), showSetDanmaku()},
|
||||
dense: true,
|
||||
enabled: false,
|
||||
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
||||
title: Text('弹幕设置', style: titleStyle),
|
||||
),
|
||||
@ -167,26 +182,38 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
/// 选择倍速
|
||||
void showSetSpeedSheet() {
|
||||
double currentSpeed = widget.controller!.playbackSpeed;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('播放速度'),
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
|
||||
content: StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
Text('$currentSpeed倍'),
|
||||
Slider(
|
||||
min: PlaySpeed.values.first.value,
|
||||
max: PlaySpeed.values.last.value,
|
||||
value: currentSpeed,
|
||||
divisions: PlaySpeed.values.length - 1,
|
||||
label: '${currentSpeed}x',
|
||||
onChanged: (double val) =>
|
||||
{setState(() => currentSpeed = val)},
|
||||
)
|
||||
for (var i in speedsList) ...[
|
||||
if (i == currentSpeed) ...[
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
// setState(() => currentSpeed = i),
|
||||
await widget.controller!.setPlaybackSpeed(i);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
] else ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
// setState(() => currentSpeed = i),
|
||||
await widget.controller!.setPlaybackSpeed(i);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(i.toString()),
|
||||
),
|
||||
]
|
||||
]
|
||||
],
|
||||
);
|
||||
}),
|
||||
@ -200,10 +227,10 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await SmartDialog.dismiss();
|
||||
widget.controller!.setPlaybackSpeed(currentSpeed);
|
||||
await widget.controller!.setDefaultSpeed();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
child: const Text('默认速度'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -257,7 +284,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('选择画质', style: titleStyle),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
@ -454,6 +481,300 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 弹幕功能
|
||||
void showSetDanmaku() async {
|
||||
// 屏蔽类型
|
||||
List<Map<String, dynamic>> blockTypesList = [
|
||||
{'value': 5, 'label': '顶部'},
|
||||
{'value': 2, 'label': '滚动'},
|
||||
{'value': 4, 'label': '底部'},
|
||||
{'value': 6, 'label': '彩色'},
|
||||
];
|
||||
List blockTypes = widget.controller!.blockTypes;
|
||||
// 显示区域
|
||||
List<Map<String, dynamic>> showAreas = [
|
||||
{'value': 0.25, 'label': '1/4屏'},
|
||||
{'value': 0.5, 'label': '半屏'},
|
||||
{'value': 0.75, 'label': '3/4屏'},
|
||||
{'value': 1.0, 'label': '满屏'},
|
||||
];
|
||||
double showArea = widget.controller!.showArea;
|
||||
// 不透明度
|
||||
double opacityVal = widget.controller!.opacityVal;
|
||||
// 字体大小
|
||||
double fontSizeVal = widget.controller!.fontSizeVal;
|
||||
// 弹幕速度
|
||||
double danmakuSpeedVal = widget.controller!.danmakuSpeedVal;
|
||||
|
||||
DanmakuController danmakuController = widget.controller!.danmakuController!;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 580,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: Center(child: Text('弹幕设置', style: titleStyle)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text('按类型屏蔽'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i in blockTypesList) ...[
|
||||
ActionRowLineItem(
|
||||
onTap: () async {
|
||||
bool isChoose = blockTypes.contains(i['value']);
|
||||
if (isChoose) {
|
||||
blockTypes.remove(i['value']);
|
||||
} else {
|
||||
blockTypes.add(i['value']);
|
||||
}
|
||||
widget.controller!.blockTypes = blockTypes;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(
|
||||
hideTop: blockTypes.contains(5),
|
||||
hideBottom: blockTypes.contains(4),
|
||||
hideScroll: blockTypes.contains(2),
|
||||
// 添加或修改其他需要修改的选项属性
|
||||
);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
text: i['label'],
|
||||
selectStatus: blockTypes.contains(i['value']),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('显示区域'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i in showAreas) ...[
|
||||
ActionRowLineItem(
|
||||
onTap: () {
|
||||
showArea = i['value'];
|
||||
widget.controller!.showArea = showArea;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(area: i['value']);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
text: i['label'],
|
||||
selectStatus: showArea == i['value'],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
Text('不透明度 ${opacityVal * 100}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
value: opacityVal,
|
||||
divisions: 10,
|
||||
label: '${opacityVal * 100}%',
|
||||
onChanged: (double val) {
|
||||
opacityVal = val;
|
||||
widget.controller!.opacityVal = opacityVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(opacity: val);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
value: fontSizeVal,
|
||||
divisions: 20,
|
||||
label: '${(fontSizeVal * 100).toStringAsFixed(1)}%',
|
||||
onChanged: (double val) {
|
||||
fontSizeVal = val;
|
||||
widget.controller!.fontSizeVal = fontSizeVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(
|
||||
fontSize: (15 * fontSizeVal).toDouble(),
|
||||
);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('弹幕时长 ${danmakuSpeedVal.toString()}'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 1,
|
||||
max: 8,
|
||||
value: danmakuSpeedVal,
|
||||
divisions: 14,
|
||||
label: danmakuSpeedVal.toString(),
|
||||
onChanged: (double val) {
|
||||
danmakuSpeedVal = val;
|
||||
widget.controller!.danmakuSpeedVal = danmakuSpeedVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(duration: val);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放顺序
|
||||
void showSetRepeat() async {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 250,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: Center(child: Text('选择播放顺序', style: titleStyle))),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView(
|
||||
children: [
|
||||
for (var i in PlayRepeat.values) ...[
|
||||
ListTile(
|
||||
onTap: () {
|
||||
widget.controller!.setPlayRepeat(i);
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 20, right: 20),
|
||||
title: Text(i.description),
|
||||
trailing: widget.controller!.playRepeat == i
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: const SizedBox(),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _ = widget.controller!;
|
||||
@ -480,7 +801,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
fuc: () => Get.back(),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.house,
|
||||
@ -525,7 +846,40 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
if (Platform.isAndroid) ...[
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () async {
|
||||
bool canUsePiP = false;
|
||||
widget.controller!.hiddenControls(false);
|
||||
try {
|
||||
canUsePiP = await widget.floating!.isPipAvailable;
|
||||
} on PlatformException catch (_) {
|
||||
canUsePiP = false;
|
||||
}
|
||||
if (canUsePiP) {
|
||||
final aspectRatio = Rational(
|
||||
widget.videoDetailCtr!.data.dash!.video!.first.width!,
|
||||
widget.videoDetailCtr!.data.dash!.video!.first.height!,
|
||||
);
|
||||
await widget.floating!.enable(aspectRatio: aspectRatio);
|
||||
} else {}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_outlined,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: buttonSpace),
|
||||
],
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: 45,
|
||||
@ -542,7 +896,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(width: buttonSpace),
|
||||
ComBtn(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.sliders,
|
||||
@ -556,3 +910,21 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MSliderTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
SliderThemeData? sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
const double trackHeight = 3;
|
||||
final double trackLeft = offset.dx;
|
||||
final double trackTop =
|
||||
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
|
||||
final double trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:pilipala/pages/home/index.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
import 'package:pilipala/utils/cookie.dart';
|
||||
import 'package:pilipala/utils/event_bus.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/login.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
@ -41,7 +42,7 @@ class WebviewController extends GetxController {
|
||||
|
||||
webviewInit() {
|
||||
controller
|
||||
..setUserAgent(Request().headerUa('mob'))
|
||||
..setUserAgent(Request().headerUa())
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
@ -50,7 +51,19 @@ class WebviewController extends GetxController {
|
||||
// Update loading bar.
|
||||
loadProgress.value = progress;
|
||||
},
|
||||
onPageStarted: (String url) {},
|
||||
onPageStarted: (String url) {
|
||||
String str = Uri.parse(url).pathSegments[0];
|
||||
Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
Get.offAndToNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 加载完成
|
||||
onUrlChange: (UrlChange urlChange) async {
|
||||
loadShow.value = false;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'controller.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
@ -24,11 +25,20 @@ class _WebviewPageState extends State<WebviewPage> {
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_webviewController.controller.reload();
|
||||
},
|
||||
child: const Text('刷新'),
|
||||
icon: Icon(Icons.refresh_outlined,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse(_webviewController.url));
|
||||
},
|
||||
icon: Icon(Icons.open_in_browser_outlined,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Obx(
|
||||
() => _webviewController.type.value == 'login'
|
||||
@ -38,7 +48,7 @@ class _WebviewPageState extends State<WebviewPage> {
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
const SizedBox(width: 10)
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
|
||||
Reference in New Issue
Block a user