Merge branch 'main' into fix

This commit is contained in:
guozhigq
2024-07-23 23:30:54 +08:00
32 changed files with 1617 additions and 726 deletions

View File

@ -6,9 +6,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
@ -16,7 +14,6 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
@ -282,4 +279,11 @@ class DynamicsController extends GetxController {
dynamicsList.value = <DynamicItemModel>[];
queryFollowDynamic();
}
// 点击up主
void onTapUp(data) {
mid.value = data.mid;
upInfo.value = data;
onSelectUp(data.mid);
}
}

View File

@ -106,7 +106,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
}
// 查看二级评论
void replyReply(replyItem) {
void replyReply(replyItem, currentReply) {
int oid = replyItem.oid;
int rpid = replyItem.rpid!;
Get.to(
@ -324,8 +324,8 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
replyItem: replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyReply: (replyItem, currentReply) =>
replyReply(replyItem, currentReply),
replyType: ReplyType.values[replyType],
addReply: (replyItem) {
replyList[index]

View File

@ -0,0 +1,46 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
class UpDynamicsController extends GetxController {
UpDynamicsController(this.upInfo);
UpItem upInfo;
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
RxBool isLoadingDynamic = false.obs;
String? offset = '';
int page = 1;
Future queryFollowDynamic({type = 'init'}) async {
if (type == 'init') {
dynamicsList.clear();
}
// 下拉刷新数据渲染时会触发onLoad
if (type == 'onLoad' && page == 1) {
return;
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: 'all',
offset: offset,
mid: upInfo.mid,
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'init') {
dynamicsList.value = res['data'].items;
} else {
dynamicsList.addAll(res['data'].items);
}
offset = res['data'].offset;
page++;
}
return res;
}
}

View File

@ -1,4 +1,4 @@
library preview;
library up_dynamics;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,151 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/utils/feed_back.dart';
import '../controller.dart';
import 'index.dart';
class OverlayPanel extends StatefulWidget {
const OverlayPanel({super.key, required this.ctr, required this.upInfo});
final DynamicsController ctr;
final UpItem upInfo;
@override
State<OverlayPanel> createState() => _OverlayPanelState();
}
class _OverlayPanelState extends State<OverlayPanel>
with SingleTickerProviderStateMixin {
static const itemPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 0);
final PageController pageController = PageController();
late double contentWidth = 50;
late List<UpItem> upList;
late RxInt currentMid = (-1).obs;
TabController? _tabController;
@override
void initState() {
super.initState();
upList = widget.ctr.upData.value.upList!
.map<UpItem>((element) => element)
.toList();
upList.removeAt(0);
_tabController = TabController(length: upList.length, vsync: this);
currentMid.value = widget.upInfo.mid!;
pageController.addListener(() {
int index = pageController.page!.round();
int mid = upList[index].mid!;
if (mid != currentMid.value) {
currentMid.value = mid;
_tabController?.animateTo(index,
duration: Duration.zero, curve: Curves.linear);
onClickUp(upList[index], index, type: 'pageChange');
}
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
int index =
upList.indexWhere((element) => element.mid == widget.upInfo.mid);
pageController.jumpToPage(index);
onClickUp(widget.upInfo, index);
_tabController?.animateTo(index,
duration: Duration.zero, curve: Curves.linear);
onClickUp(upList[index], index, type: 'pageChange');
});
}
void onClickUp(data, i, {type = 'click'}) {
if (type == 'click') {
pageController.jumpToPage(i);
}
}
@override
Widget build(BuildContext context) {
return Container(
width: Get.width,
height: Get.height,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.fromLTRB(
0,
MediaQuery.of(context).padding.top + 4,
0,
MediaQuery.of(context).padding.bottom + 4,
),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
SizedBox(
height: 50,
child: TabBar(
controller: _tabController,
dividerColor: Colors.transparent,
automaticIndicatorColorAdjustment: false,
tabAlignment: TabAlignment.start,
padding: const EdgeInsets.only(left: 12, right: 12),
indicatorPadding: EdgeInsets.zero,
indicatorSize: TabBarIndicatorSize.label,
indicator: const BoxDecoration(),
labelPadding: itemPadding,
indicatorWeight: 1,
isScrollable: true,
tabs: upList.map((e) => Tab(child: upItemBuild(e))).toList(),
onTap: (index) {
feedBack();
EasyThrottle.throttle(
'follow', const Duration(milliseconds: 200), () {
onClickUp(upList[index], index);
});
},
),
),
Expanded(
child: PageView.builder(
itemCount: upList.length,
controller: pageController,
itemBuilder: (BuildContext context, int index) {
return Container(
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.fromLTRB(10, 12, 10, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
),
child: UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr),
);
},
),
),
],
),
);
}
Widget upItemBuild(data) {
return Obx(
() => AnimatedOpacity(
opacity: currentMid == data.mid ? 1 : 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: currentMid == data.mid ? 1 : 0.9,
child: NetworkImgLayer(
width: contentWidth,
height: contentWidth,
src: data.face,
type: 'avatar',
),
),
),
);
}
}

View File

@ -0,0 +1,178 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/pages/dynamics/up_dynamic/index.dart';
import '../index.dart';
import '../widgets/dynamic_panel.dart';
class UpDyanmicsPage extends StatefulWidget {
final UpItem upInfo;
final DynamicsController ctr;
const UpDyanmicsPage({
required this.upInfo,
required this.ctr,
Key? key,
}) : super(key: key);
@override
State<UpDyanmicsPage> createState() => _UpDyanmicsPageState();
}
class _UpDyanmicsPageState extends State<UpDyanmicsPage>
with AutomaticKeepAliveClientMixin {
late UpDynamicsController _upDynamicsController;
final ScrollController scrollController = ScrollController();
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_upDynamicsController = Get.put(UpDynamicsController(widget.upInfo),
tag: widget.upInfo.mid.toString());
_futureBuilderFuture = _upDynamicsController.queryFollowDynamic();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
_upDynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
},
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return CustomScrollView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
height: 50,
padding: const EdgeInsets.fromLTRB(20, 4, 4, 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.onSurface,
width: 0.1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.upInfo.uname!,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
)
],
),
),
),
),
FutureBuilder(
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 != null && data['status']) {
List<DynamicItemModel> list =
_upDynamicsController.dynamicsList;
return Obx(
() {
if (list.isEmpty) {
if (_upDynamicsController.isLoadingDynamic.value) {
return skeleton();
} else {
return const NoData();
}
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
},
childCount: list.length,
),
);
}
},
);
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {},
);
}
} else {
// 骨架屏
return skeleton();
}
},
),
],
);
}
Widget skeleton() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 5),
);
}
}
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_MySliverPersistentHeaderDelegate({required this.child});
final double _minExtent = 50;
final double _maxExtent = 50;
final Widget child;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
@override
double get maxExtent => _maxExtent;
@override
double get minExtent => _minExtent;
@override
bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}

View File

@ -3,13 +3,13 @@ import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/route_push.dart';
@ -18,6 +18,7 @@ import 'package:pilipala/utils/storage.dart';
import '../mine/controller.dart';
import 'controller.dart';
import 'widgets/dynamic_panel.dart';
import 'up_dynamic/route_panel.dart';
import 'widgets/up_panel.dart';
class DynamicsPage extends StatefulWidget {
@ -202,7 +203,21 @@ class _DynamicsPageState extends State<DynamicsPage>
}
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value));
return Obx(
() => UpPanel(
upData: _dynamicsController.upData.value,
onClickUpCb: (data) {
// _dynamicsController.onTapUp(data);
Navigator.push(
context,
PlPopupRoute(
child: OverlayPanel(
ctr: _dynamicsController, upInfo: data),
),
);
},
),
);
} else {
return const SliverToBoxAdapter(
child: SizedBox(height: 80),

View File

@ -1,11 +1,11 @@
// 内容
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/badge.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 'package:pilipala/plugin/pl_gallery/index.dart';
import 'rich_node_panel.dart';
// ignore: must_be_immutable
@ -59,17 +59,15 @@ class _ContentState extends State<Content> {
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1);
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
return Hero(
tag: pictureItem.url!,
placeholderBuilder:
(BuildContext context, Size heroSize, Widget child) {
return child;
},
child: Container(
child: GestureDetector(
onTap: () => onPreviewImg(picList, 1, context),
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
@ -91,7 +89,9 @@ class _ContentState extends State<Content> {
)
: const SizedBox(),
],
)),
),
),
),
);
},
),
@ -102,26 +102,23 @@ class _ContentState extends State<Content> {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pics[i].url!);
}
for (var i = 0; i < len; i++) {
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth.truncateToDouble();
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: pics[i].url,
width: maxWidth,
height: maxWidth,
origAspectRatio:
pics[i].width!.toInt() / pics[i].height!.toInt(),
return Hero(
tag: picList[i],
child: GestureDetector(
onTap: () => onPreviewImg(picList, i, context),
child: NetworkImgLayer(
src: pics[i].url,
width: maxWidth,
height: maxWidth,
origAspectRatio:
pics[i].width!.toInt() / pics[i].height!.toInt(),
),
),
);
},
@ -163,6 +160,43 @@ class _ContentState extends State<Content> {
);
}
void onPreviewImg(picList, initIndex, context) {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
}
@override
Widget build(BuildContext context) {
TextStyle authorStyle =

View File

@ -1,9 +1,47 @@
import 'package:cached_network_image/cached_network_image.dart';
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';
import 'package:pilipala/plugin/pl_gallery/index.dart';
void onPreviewImg(currentUrl, picList, initIndex, context) {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
}
Widget picWidget(item, context) {
String type = item.modules.moduleDynamic.major.type;
@ -21,25 +59,25 @@ Widget picWidget(item, context) {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pictures[i].src ?? pictures[i].url);
}
for (var i = 0; i < len; i++) {
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
return Hero(
tag: picList[i],
placeholderBuilder:
(BuildContext context, Size heroSize, Widget child) {
return child;
},
child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth,
height: box.maxWidth,
child: GestureDetector(
onTap: () => onPreviewImg(picList[i], picList, i, context),
child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth,
height: box.maxWidth,
),
),
// ),
);
},
),

View File

@ -4,13 +4,18 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/dynamics/controller.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart';
class UpPanel extends StatefulWidget {
final FollowUpModel upData;
const UpPanel(this.upData, {Key? key}) : super(key: key);
final Function? onClickUpCb;
const UpPanel({
super.key,
required this.upData,
this.onClickUpCb,
});
@override
State<UpPanel> createState() => _UpPanelState();
@ -33,27 +38,25 @@ class _UpPanelState extends State<UpPanel> {
void onClickUp(data, i) {
currentMid = data.mid;
Get.find<DynamicsController>().mid.value = data.mid;
Get.find<DynamicsController>().upInfo.value = data;
Get.find<DynamicsController>().onSelectUp(data.mid);
int liveLen = liveList.length;
int upLen = upList.length;
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.sizeOf(context).width;
double moveDistance = 0.0;
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
);
setState(() {});
widget.onClickUpCb?.call(data);
// int liveLen = liveList.length;
// int upLen = upList.length;
// double itemWidth = contentWidth + itemPadding.horizontal;
// double screenWidth = MediaQuery.sizeOf(context).width;
// double moveDistance = 0.0;
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
// moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
// } else {
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
// }
// data.hasUpdate = false;
// scrollController.animateTo(
// moveDistance,
// duration: const Duration(milliseconds: 200),
// curve: Curves.linear,
// );
// setState(() {});
}
@override

View File

@ -27,6 +27,7 @@ class MainController extends GetxController {
RxBool userLogin = false.obs;
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
late bool enableGradientBg;
bool imgPreviewStatus = false;
@override
void onInit() {

View File

@ -9,12 +9,14 @@ class MemberArchiveController extends GetxController {
int pn = 1;
int count = 0;
RxMap<String, String> currentOrder = <String, String>{}.obs;
List<Map<String, String>> orderList = [
RxList<Map<String, String>> orderList = [
{'type': 'pubdate', 'label': '最新发布'},
{'type': 'click', 'label': '最多播放'},
{'type': 'stow', 'label': '最多收藏'},
];
{'type': 'charge', 'label': '充电专属'},
].obs;
RxList<VListItemModel> archivesList = <VListItemModel>[].obs;
RxBool isLoading = false.obs;
@override
void onInit() {
@ -27,6 +29,8 @@ class MemberArchiveController extends GetxController {
Future getMemberArchive(type) async {
if (type == 'init') {
pn = 1;
archivesList.clear();
isLoading.value = true;
}
var res = await MemberHttp.memberArchive(
mid: mid,
@ -43,6 +47,7 @@ class MemberArchiveController extends GetxController {
count = res['data'].page['count'];
pn += 1;
}
isLoading.value = false;
return res;
}

View File

@ -1,6 +1,8 @@
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/common/widgets/video_card_h.dart';
import 'package:pilipala/utils/utils.dart';
import '../../common/widgets/http_error.dart';
@ -47,14 +49,29 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
appBar: AppBar(
titleSpacing: 0,
centerTitle: false,
title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium),
title: Obx(
() => Text(
'他的投稿 - ${_memberArchivesController.currentOrder['label']}',
style: Theme.of(context).textTheme.titleMedium),
),
actions: [
Obx(
() => TextButton.icon(
icon: const Icon(Icons.sort, size: 20),
onPressed: _memberArchivesController.toggleSort,
label: Text(_memberArchivesController.currentOrder['label']!),
),
// Obx(
PopupMenuButton(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
// 这里处理选择逻辑
_memberArchivesController.currentOrder.value = value;
_memberArchivesController.getMemberArchive('init');
},
itemBuilder: (BuildContext context) =>
_memberArchivesController.orderList.map(
(e) {
return PopupMenuItem(
value: e,
child: Text(e['label']!),
);
},
).toList(),
),
const SizedBox(width: 6),
],
@ -85,7 +102,14 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
childCount: list.length,
),
)
: const SliverToBoxAdapter(),
: _memberArchivesController.isLoading.value
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
)
: const NoData(),
);
} else {
return HttpError(
@ -100,7 +124,11 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
);
}
} else {
return const SliverToBoxAdapter();
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
),

View File

@ -1,50 +0,0 @@
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';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
class PreviewController extends GetxController {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
RxInt initialPage = 0.obs;
RxInt currentPage = 1.obs;
RxList imgList = [].obs;
bool storage = true;
bool videos = true;
bool photos = true;
String currentImgUrl = '';
requestPermission() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
// Permission.photos
].request();
statuses[Permission.storage].toString();
// final photosInfo = statuses[Permission.photos].toString();
}
// 图片分享
void onShareImg() async {
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';
File(path).writeAsBytesSync(response.data);
Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
}
void onChange(int index) {
initialPage.value = index;
currentPage.value = index + 1;
currentImgUrl = imgList[index];
}
}

View File

@ -1,290 +0,0 @@
// ignore_for_file: library_private_types_in_public_api
import 'dart:io';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart';
import 'package:pilipala/utils/download.dart';
import 'controller.dart';
import 'package:status_bar_control/status_bar_control.dart';
typedef DoubleClickAnimationListener = void Function();
class ImagePreview extends StatefulWidget {
final int? initialPage;
final List<String>? imgList;
const ImagePreview({
Key? key,
this.initialPage,
this.imgList,
}) : super(key: key);
@override
_ImagePreviewState createState() => _ImagePreviewState();
}
class _ImagePreviewState extends State<ImagePreview>
with TickerProviderStateMixin {
final PreviewController _previewController = Get.put(PreviewController());
// late AnimationController animationController;
late AnimationController _doubleClickAnimationController;
Animation<double>? _doubleClickAnimation;
late DoubleClickAnimationListener _doubleClickAnimationListener;
List<double> doubleTapScales = <double>[1.0, 2.0];
bool _dismissDisabled = false;
@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();
_doubleClickAnimationController = AnimationController(
duration: const Duration(milliseconds: 250), vsync: this);
}
onOpenMenu() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
_previewController.onShareImg();
Get.back();
},
dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Clipboard.setData(
ClipboardData(text: _previewController.currentImgUrl))
.then((value) {
Get.back();
SmartDialog.showToast('已复制到粘贴板');
}).catchError((err) {
SmartDialog.showNotify(
msg: err.toString(),
notifyType: NotifyType.error,
);
});
},
dense: true,
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Get.back();
DownloadUtils.downloadImg(_previewController.currentImgUrl);
},
dense: true,
title: const Text('保存到手机', style: TextStyle(fontSize: 14)),
),
],
),
);
},
);
}
// 隐藏状态栏,避免遮挡图片内容
setStatusBar() async {
if (Platform.isIOS || Platform.isAndroid) {
await StatusBarControl.setHidden(true,
animation: StatusBarAnimation.SLIDE);
}
}
@override
void dispose() {
// animationController.dispose();
try {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
} catch (_) {}
_doubleClickAnimationController.dispose();
clearGestureDetailsCache();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
primary: false,
extendBody: true,
appBar: AppBar(
primary: false,
toolbarHeight: 0,
backgroundColor: Colors.black,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
body: Stack(
children: [
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,
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,
);
},
);
},
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: EdgeInsets.only(
left: 20,
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black87,
],
tileMode: TileMode.mirror,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.imgList!.length > 1
? Obx(
() => Text.rich(
textAlign: TextAlign.center,
TextSpan(
style: const TextStyle(
color: Colors.white, fontSize: 16),
children: [
TextSpan(
text: _previewController.currentPage
.toString()),
const TextSpan(text: ' / '),
TextSpan(
text:
widget.imgList!.length.toString()),
]),
),
)
: const SizedBox(),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close, color: Colors.white),
),
],
)),
),
],
),
);
}
}

View File

@ -162,7 +162,7 @@ class VideoDetailController extends GetxController
);
}
showReplyReplyPanel(oid, fRpid, firstFloor) {
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
replyReplyBottomSheetCtr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel(
@ -175,6 +175,8 @@ class VideoDetailController extends GetxController
replyType: ReplyType.video,
source: 'videoDetail',
sheetHeight: sheetHeight.value,
currentReply: currentReply,
loadMore: loadMore,
);
});
replyReplyBottomSheetCtr?.closed.then((value) {

View File

@ -112,7 +112,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
}
// 展示二级回复
void replyReply(replyItem) {
void replyReply(replyItem, currentReply, loadMore) {
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) {
@ -120,7 +120,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel(
replyItem.oid, replyItem.rpid!, replyItem);
replyItem.oid, replyItem.rpid!, replyItem, currentReply, loadMore);
}
}
@ -232,8 +232,10 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
.replyList[index],
showReplyRow: true,
replyLevel: replyLevel,
replyReply: (replyItem) =>
replyReply(replyItem),
replyReply: (replyItem, currentReply,
loadMore) =>
replyReply(replyItem, currentReply,
loadMore),
replyType: ReplyType.video,
);
}

View File

@ -1,4 +1,5 @@
import 'package:appscheme/appscheme.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -9,9 +10,10 @@ 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/main/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
@ -47,7 +49,7 @@ class ReplyItem extends StatelessWidget {
onTap: () {
feedBack();
if (replyReply != null) {
replyReply!(replyItem);
replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);
}
},
onLongPress: () {
@ -360,9 +362,13 @@ class ReplyItemRow extends StatelessWidget {
for (int i = 0; i < replies!.length; i++) ...[
InkWell(
// 一楼点击评论展开评论详情
// onTap: () {
// replyReply?.call(replyItem);
// },
onTap: () {
replyReply?.call(
replyItem,
replies![i],
replyItem!.replies!.isNotEmpty,
);
},
onLongPress: () {
feedBack();
showModalBottomSheet(
@ -533,9 +539,59 @@ InlineSpan buildContent(
spanChilds.add(
TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem),
// recognizer: TapGestureRecognizer()
// ..onTap = () => replyReply?.call(
// replyItem.root == 0 ? replyItem : fReplyItem,
// replyItem,
// fReplyItem!.replies!.isNotEmpty,
// ),
),
);
}
void onPreviewImg(picList, initIndex) {
final MainController mainController = Get.find<MainController>();
mainController.imgPreviewStatus = true;
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
final MainController mainController =
Get.find<MainController>();
mainController.imgPreviewStatus = false;
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
onDismissed: (int value) {
print('onDismissed');
final MainController mainController = Get.find<MainController>();
mainController.imgPreviewStatus = false;
},
),
),
);
}
@ -831,38 +887,33 @@ InlineSpan buildContent(
.truncateToDouble();
} catch (_) {}
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (BuildContext context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
},
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: pictureItem['img_src'],
width: box.maxWidth / 2,
height: height,
return Hero(
tag: picList[0],
child: GestureDetector(
onTap: () => onPreviewImg(picList, 0),
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: picList[0],
width: box.maxWidth / 2,
height: height,
),
),
),
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,
bottom: 8,
)
: const SizedBox(),
],
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,
bottom: 8,
)
: const SizedBox(),
],
),
),
),
);
@ -874,25 +925,22 @@ InlineSpan buildContent(
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(content.pictures[i]['img_src']);
}
for (var i = 0; i < len; i++) {
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: content.pictures[i]['img_src'],
width: box.maxWidth,
height: box.maxWidth,
origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
return Hero(
tag: picList[i],
child: GestureDetector(
onTap: () => onPreviewImg(picList, i),
child: NetworkImgLayer(
src: picList[i],
width: box.maxWidth,
height: box.maxWidth,
origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
),
);
},
),

View File

@ -26,7 +26,7 @@ class VideoReplyReplyController extends GetxController {
currentPage = 0;
}
Future queryReplyList({type = 'init'}) async {
Future queryReplyList({type = 'init', currentReply}) async {
if (type == 'init') {
currentPage = 0;
}
@ -63,6 +63,17 @@ class VideoReplyReplyController extends GetxController {
// res['data'].replies.addAll(replyList);
}
}
if (replyList.isNotEmpty && currentReply != null) {
int indexToRemove =
replyList.indexWhere((item) => currentReply.rpid == item.rpid);
// 如果找到了指定ID的项则移除
if (indexToRemove != -1) {
replyList.removeAt(indexToRemove);
}
if (currentPage == 1 && type == 'init') {
replyList.insert(0, currentReply);
}
}
isLoadingMore = false;
return res;
}

View File

@ -20,6 +20,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.source,
this.replyType,
this.sheetHeight,
this.currentReply,
this.loadMore,
super.key,
});
final int? oid;
@ -29,6 +31,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
final String? source;
final ReplyType? replyType;
final double? sheetHeight;
final dynamic currentReply;
final bool? loadMore;
@override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@ -63,7 +67,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
},
);
_futureBuilderFuture = _videoReplyReplyController.queryReplyList();
_futureBuilderFuture = _videoReplyReplyController.queryReplyList(
currentReply: widget.currentReply,
);
}
void replyReply(replyItem) {}
@ -107,7 +113,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
onRefresh: () async {
setState(() {});
_videoReplyReplyController.currentPage = 0;
return await _videoReplyReplyController.queryReplyList();
return await _videoReplyReplyController.queryReplyList(
currentReply: widget.currentReply,
);
},
child: CustomScrollView(
controller: _videoReplyReplyController.scrollController,
@ -134,84 +142,102 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
),
),
],
FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
// 请求成功
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index ==
_videoReplyReplyController
.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(
widget.loadMore != null && widget.loadMore!
? FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
// 请求成功
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index ==
_videoReplyReplyController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
.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(
_videoReplyReplyController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _videoReplyReplyController
.replyList[index],
replyLevel: '2',
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController.replyList
.add(replyItem);
);
} else {
return ReplyItem(
replyItem:
_videoReplyReplyController
.replyList[index],
replyLevel: '2',
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController
.replyList
.add(replyItem);
},
replyType: widget.replyType,
replyReply: (replyItem) =>
replyReply(replyItem),
);
}
},
replyType: widget.replyType,
replyReply: (replyItem) =>
replyReply(replyItem),
);
}
},
childCount: _videoReplyReplyController
.replyList.length +
1,
childCount: _videoReplyReplyController
.replyList.length +
1,
),
),
);
} else {
// 请求错误
return HttpError(
errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
: SliverToBoxAdapter(
child: SizedBox(
height: 200,
child: Center(
child: Text(
'还没有评论',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
);
} else {
// 请求错误
return HttpError(
errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
),
)
],
),
),

View File

@ -15,6 +15,7 @@ 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/main/index.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';
@ -240,6 +241,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
// 离开当前页面时
void didPushNext() async {
final MainController mainController = Get.find<MainController>();
if (mainController.imgPreviewStatus) {
return;
}
/// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
as bool) {
@ -259,6 +265,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
// 返回当前页面时
void didPopNext() async {
final MainController mainController = Get.find<MainController>();
if (mainController.imgPreviewStatus) {
return;
}
if (plPlayerController != null &&
plPlayerController!.videoPlayerController != null) {
setState(() {