refa: dir

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-03 13:57:47 +08:00
parent 57fa8b4f3e
commit 7f70ee5045
260 changed files with 748 additions and 967 deletions

View File

@@ -1,8 +1,10 @@
import 'dart:async';
import 'package:PiliPlus/http/follow.dart';
import 'package:PiliPlus/pages/dynamics/tab/controller.dart';
import 'package:PiliPlus/pages/dynamics/tab/view.dart';
import 'package:PiliPlus/models/follow/result.dart';
import 'package:PiliPlus/pages/common/common_controller.dart';
import 'package:PiliPlus/pages/dynamics_tab/controller.dart';
import 'package:PiliPlus/pages/dynamics_tab/view.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -12,9 +14,6 @@ import 'package:PiliPlus/models/common/dynamics_type.dart';
import 'package:PiliPlus/models/dynamics/up.dart';
import 'package:PiliPlus/utils/storage.dart';
import '../../models/follow/result.dart';
import '../common/common_controller.dart';
class DynamicsController extends GetxController
with GetSingleTickerProviderStateMixin, ScrollOrRefreshMixin {
@override

View File

@@ -1,498 +0,0 @@
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics/view.dart';
import 'package:PiliPlus/pages/emote/controller.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
class CreateDynPanel extends CommonPublishPage {
const CreateDynPanel({
super.key,
super.imageLengthLimit = 18,
});
@override
State<CreateDynPanel> createState() => _CreateDynPanelState();
}
class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
bool _isPrivate = false;
DateTime? _publishTime;
ReplyOption _replyOption = ReplyOption.allow;
@override
void dispose() {
try {
Get.delete<EmotePanelController>();
} catch (_) {}
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
appBar: _buildAppBar(theme),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildEditWidget,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildPubtimeWidget,
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReplyOptionWidget(theme),
const SizedBox(height: 5),
_buildPrivateWidget(theme),
],
),
],
),
),
const SizedBox(height: 10),
_buildImageList(theme),
const SizedBox(height: 2),
],
),
),
_buildToolbar,
buildPanelContainer(Colors.transparent),
],
),
);
}
Widget _buildImageList(ThemeData theme) => Obx(
() => SizedBox(
height: 100,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: pathList.length == limit ? limit : pathList.length + 1,
itemBuilder: (context, index) {
if (pathList.length != limit && index == pathList.length) {
return Material(
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
onPickImage(() {
if (pathList.isNotEmpty && !enablePublish.value) {
enablePublish.value = true;
}
});
},
child: Ink(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.secondaryContainer,
),
child: Center(child: Icon(Icons.add, size: 35)),
),
),
);
} else {
return buildImage(index, 100);
}
},
separatorBuilder: (context, index) => const SizedBox(width: 10),
),
),
);
PreferredSizeWidget _buildAppBar(ThemeData theme) => PreferredSize(
preferredSize: Size.fromHeight(66),
child: Padding(
padding: const EdgeInsets.all(16),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 34,
height: 34,
child: IconButton(
tooltip: '返回',
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
backgroundColor: WidgetStateProperty.resolveWith(
(states) {
return theme.colorScheme.secondaryContainer;
},
),
),
onPressed: Get.back,
icon: Icon(
Icons.arrow_back_outlined,
size: 18,
color: theme.colorScheme.onSecondaryContainer,
),
),
),
),
Center(
child: const Text(
'发布动态',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
),
Align(
alignment: Alignment.centerRight,
child: Obx(
() => FilledButton.tonal(
onPressed: enablePublish.value ? onPublish : null,
style: FilledButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
),
child: Text(_publishTime == null ? '发布' : '定时发布'),
),
),
),
],
),
),
);
Widget _buildPrivateWidget(ThemeData theme) {
final color =
_isPrivate ? theme.colorScheme.error : theme.colorScheme.secondary;
return PopupMenuButton(
initialValue: _isPrivate,
onOpened: controller.keepChatPanel,
onSelected: (value) {
setState(() {
_isPrivate = value;
});
},
itemBuilder: (context) => List.generate(
2,
(index) => PopupMenuItem<bool>(
enabled: _publishTime != null && index == 1 ? false : true,
value: index == 0 ? false : true,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
index == 0 ? Icons.visibility : Icons.visibility_off,
),
const SizedBox(width: 4),
Text(index == 0 ? '所有人可见' : '仅自己可见'),
],
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
_isPrivate ? Icons.visibility_off : Icons.visibility,
color: color,
),
const SizedBox(width: 4),
Text(
_isPrivate ? '仅自己可见' : '所有人可见',
style: TextStyle(
height: 1,
color: color,
),
strutStyle: StrutStyle(leading: 0, height: 1),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: color,
),
],
),
),
);
}
Widget _buildReplyOptionWidget(ThemeData theme) {
final color = _replyOption == ReplyOption.close
? theme.colorScheme.error
: theme.colorScheme.secondary;
return PopupMenuButton(
initialValue: _replyOption,
onOpened: controller.keepChatPanel,
onSelected: (item) {
setState(() {
_replyOption = item;
});
},
itemBuilder: (context) => ReplyOption.values
.map(
(item) => PopupMenuItem<ReplyOption>(
value: item,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
item.iconData,
),
const SizedBox(width: 4),
Text(item.title),
],
),
),
)
.toList(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 19,
_replyOption.iconData,
color: color,
),
const SizedBox(width: 4),
Text(
_replyOption.title,
style: TextStyle(
height: 1,
color: color,
),
strutStyle: StrutStyle(leading: 0, height: 1),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: color,
),
],
),
),
);
}
Widget get _buildPubtimeWidget => _publishTime == null
? FilledButton.tonal(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
),
onPressed: _isPrivate
? null
: () {
DateTime nowDate = DateTime.now();
showDatePicker(
context: context,
initialDate: nowDate,
firstDate: nowDate,
lastDate: DateTime(
nowDate.year,
nowDate.month,
nowDate.day + 7,
),
).then(
(selectedDate) {
if (selectedDate != null && mounted) {
TimeOfDay nowTime = TimeOfDay.now();
showTimePicker(
context: context,
initialTime: nowTime.replacing(
hour: nowTime.minute + 6 >= 60
? (nowTime.hour + 1) % 24
: nowTime.hour,
minute: (nowTime.minute + 6) % 60,
),
).then((selectedTime) {
if (selectedTime != null) {
if (selectedDate.day == nowDate.day) {
if (selectedTime.hour < nowTime.hour) {
SmartDialog.showToast('时间设置错误至少选择6分钟之后');
return;
} else if (selectedTime.hour == nowTime.hour) {
if (selectedTime.minute < nowTime.minute + 6) {
if (selectedDate.day == nowDate.day) {
SmartDialog.showToast('时间设置错误至少选择6分钟之后');
}
return;
}
}
}
setState(() {
_publishTime = DateTime(
selectedDate.year,
selectedDate.month,
selectedDate.day,
selectedTime.hour,
selectedTime.minute,
);
});
}
});
}
},
);
},
child: const Text('定时发布'),
)
: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
),
onPressed: () {
setState(() {
_publishTime = null;
});
},
label: Text(DateFormat('yyyy-MM-dd HH:mm').format(_publishTime!)),
icon: Icon(Icons.clear, size: 20),
iconAlignment: IconAlignment.end,
);
Widget get _buildToolbar => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Obx(
() => ToolbarIconButton(
onPressed: () {
selectKeyboard.value = PanelType.emoji == currentPanelType;
updatePanelType(
PanelType.emoji == currentPanelType
? PanelType.keyboard
: PanelType.emoji,
);
},
icon: const Icon(Icons.emoji_emotions, size: 22),
tooltip: '表情',
selected: !selectKeyboard.value,
),
),
],
),
);
Widget get _buildEditWidget => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener(
onPointerUp: (event) {
if (readOnly.value) {
updatePanelType(PanelType.keyboard);
selectKeyboard.value = true;
}
},
child: Obx(
() => TextField(
controller: editController,
minLines: 4,
maxLines: null,
focusNode: focusNode,
readOnly: readOnly.value,
onChanged: (value) {
bool isEmpty = value.trim().isEmpty && pathList.isEmpty;
if (!isEmpty && !enablePublish.value) {
enablePublish.value = true;
} else if (isEmpty && enablePublish.value) {
enablePublish.value = false;
}
},
decoration: const InputDecoration(
hintText: '说点什么吧',
border: OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0,
),
contentPadding: EdgeInsets.zero,
),
),
),
),
);
@override
Widget? customPanel(double height) => SizedBox(
height: height,
child: EmotePanel(onChoose: onChooseEmote),
);
@override
Future onCustomPublish({required String message, List? pictures}) async {
SmartDialog.showLoading(msg: '正在发布');
dynamic result = await MsgHttp.createDynamic(
mid: Accounts.main.mid,
rawText: editController.text,
pics: pictures,
publishTime: _publishTime != null
? _publishTime!.millisecondsSinceEpoch ~/ 1000
: null,
replyOption: _replyOption,
privatePub: _isPrivate ? 1 : null,
);
SmartDialog.dismiss();
if (result['status']) {
Get.back();
SmartDialog.showToast('发布成功');
RequestUtils.insertCreatedDyn(result);
RequestUtils.checkCreatedDyn(
id: result['data']?['dyn_id'],
dynText: editController.text,
);
} else {
SmartDialog.showToast(result['msg']);
debugPrint('failed to publish: ${result['msg']}');
}
}
}

View File

@@ -1,67 +0,0 @@
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:fixnum/fixnum.dart' as $fixnum;
class DynamicDetailController extends ReplyController<MainListReply> {
DynamicDetailController(this.oid, this.type);
int oid;
int type;
late DynamicItemModel item;
int? floor;
late final horizontalPreview = GStorage.horizontalPreview;
late final showDynActionBar = GStorage.showDynActionBar;
@override
dynamic get sourceId =>
type == ReplyType.video.index ? IdUtils.av2bv(oid) : oid;
@override
void onInit() {
super.onInit();
item = Get.arguments['item'];
floor = Get.arguments['floor'];
if (floor == 1) {
count.value = item.modules.moduleStat?.comment?.count ?? 0;
}
if (oid != 0) {
queryData();
}
}
getCommentParams(int id) async {
var res = await DynamicsHttp.opusDetail(opusId: id);
if (res is Success) {
final data = (res as Success<DynamicItemModel>).response;
type = data.basic!.commentType!;
oid = int.parse(data.basic!.commentIdStr!);
queryData();
}
}
@override
List<ReplyInfo>? getDataList(MainListReply response) {
return response.replies;
}
@override
Future<LoadingState<MainListReply>> customGetData() =>
ReplyHttp.replyListGrpc(
type: type,
oid: oid,
cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0),
mode: mode.value,
),
antiGoodsReply: antiGoodsReply,
);
}

View File

@@ -1,4 +0,0 @@
library dynamic_detail;
export './controller.dart';
export './view.dart';

View File

@@ -1,847 +0,0 @@
import 'dart:math';
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/reply_sort_type.dart';
import 'package:PiliPlus/pages/dynamics/repost_dyn_panel.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/http_error.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/detail/index.dart';
import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart';
import 'package:PiliPlus/pages/video/detail/reply_reply/index.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import '../../../utils/grid.dart';
import '../widgets/dynamic_panel.dart';
class DynamicDetailPage extends StatefulWidget {
const DynamicDetailPage({super.key});
@override
State<DynamicDetailPage> createState() => _DynamicDetailPageState();
}
class _DynamicDetailPageState extends State<DynamicDetailPage>
with TickerProviderStateMixin {
late DynamicDetailController _dynamicDetailController;
AnimationController? _fabAnimationCtr;
final RxBool _visibleTitle = false.obs;
// 回复类型
late int replyType;
bool _isFabVisible = true;
bool? _imageStatus;
int oid = 0;
int? opusId;
bool isOpusId = false;
late final List<double> _ratio = GStorage.dynamicDetailRatio;
bool get _horizontalPreview =>
context.orientation == Orientation.landscape &&
_dynamicDetailController.horizontalPreview;
late final _key = GlobalKey<ScaffoldState>();
get _getImageCallback => _horizontalPreview
? (imgList, index) {
_imageStatus = true;
bool isFabVisible = _isFabVisible;
if (isFabVisible) {
_hideFab();
}
final ctr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
)..forward();
PageUtils.onHorizontalPreview(
_key,
AnimationController(
vsync: this,
duration: Duration.zero,
),
ctr,
imgList,
index,
(value) async {
_imageStatus = null;
if (isFabVisible) {
isFabVisible = false;
_showFab();
}
if (value == false) {
await ctr.reverse();
}
try {
ctr.dispose();
} catch (_) {}
if (value == false) {
Get.back();
}
},
);
}
: null;
@override
void initState() {
super.initState();
// floor 1原创 2转发
init();
_fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_fabAnimationCtr?.forward();
_dynamicDetailController.scrollController.addListener(listener);
}
// 页面初始化
void init() async {
Map args = Get.arguments;
// 楼层
int floor = args['floor'];
// 评论类型
final item = args['item'] as DynamicItemModel;
int commentType = item.basic?.commentType ?? 11;
replyType = (commentType == 0) ? 11 : commentType;
if (floor == 1) {
oid = int.parse(item.basic!.commentIdStr!);
} else {
try {
final moduleDynamic = item.modules.moduleDynamic!;
String majorType = moduleDynamic.major!.type!;
if (majorType == 'MAJOR_TYPE_OPUS') {
// 转发的动态
String jumpUrl = moduleDynamic.major!.opus!.jumpUrl!;
opusId = int.parse(jumpUrl.split('/').last);
if (opusId != null) {
isOpusId = true;
_dynamicDetailController = Get.put(
DynamicDetailController(oid, replyType),
tag: Utils.makeHeroTag(opusId),
);
await _dynamicDetailController.getCommentParams(opusId!);
setState(() {});
}
} else {
oid = moduleDynamic.major!.draw!.id!;
}
} catch (_) {}
}
if (!isOpusId) {
_dynamicDetailController = Get.put(
DynamicDetailController(oid, replyType),
tag: Utils.makeHeroTag(oid),
);
}
}
// 查看二级评论
void replyReply(BuildContext context, ReplyInfo replyItem, int? id) {
EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () {
int oid = replyItem.oid.toInt();
int rpid = replyItem.id.toInt();
Widget replyReplyPage(
[bool automaticallyImplyLeading = true,
VoidCallback? onDispose]) =>
Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('评论详情'),
titleSpacing: automaticallyImplyLeading ? null : 12,
automaticallyImplyLeading: automaticallyImplyLeading,
),
body: SafeArea(
top: false,
bottom: false,
child: VideoReplyReplyPanel(
id: id,
oid: oid,
rpid: rpid,
source: 'dynamic',
replyType: ReplyType.values[replyType],
firstFloor: replyItem,
onDispose: onDispose,
),
),
);
if (this.context.orientation == Orientation.portrait) {
Get.to(
replyReplyPage,
routeName: 'dynamicDetail-Copy',
arguments: {
'item': _dynamicDetailController.item,
},
);
} else {
ScaffoldState? scaffoldState = Scaffold.maybeOf(context);
if (scaffoldState != null) {
bool isFabVisible = _isFabVisible;
if (isFabVisible) {
_hideFab();
}
scaffoldState.showBottomSheet(
backgroundColor: Colors.transparent,
(context) => MediaQuery.removePadding(
context: context,
removeLeft: true,
child: replyReplyPage(
false,
() {
if (isFabVisible && _imageStatus != true) {
_showFab();
}
},
),
),
);
} else {
Get.to(
replyReplyPage,
routeName: 'dynamicDetail-Copy',
arguments: {
'item': _dynamicDetailController.item,
},
);
}
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_dynamicDetailController.scrollController.hasClients) {
_visibleTitle.value =
_dynamicDetailController.scrollController.positions.first.pixels >
55;
}
});
}
void listener() {
// 标题
_visibleTitle.value =
_dynamicDetailController.scrollController.positions.first.pixels > 55;
// fab按钮
final ScrollDirection direction1 = _dynamicDetailController
.scrollController.positions.first.userScrollDirection;
late final ScrollDirection direction2 = _dynamicDetailController
.scrollController.positions.last.userScrollDirection;
if (direction1 == ScrollDirection.forward ||
direction2 == ScrollDirection.forward) {
_showFab();
} else if (direction1 == ScrollDirection.reverse ||
direction2 == ScrollDirection.reverse) {
_hideFab();
}
}
void _showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
_fabAnimationCtr?.forward();
}
}
void _hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
_fabAnimationCtr?.reverse();
}
}
@override
void dispose() {
_fabAnimationCtr?.dispose();
_fabAnimationCtr = null;
_dynamicDetailController.scrollController.removeListener(listener);
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Padding(
padding: const EdgeInsets.only(right: 12),
child: Obx(
() {
return AnimatedOpacity(
opacity: _visibleTitle.value ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: AuthorPanel(
item: _dynamicDetailController.item,
source: 'detail', //to remove tag
),
);
},
),
),
actions: context.orientation == Orientation.landscape
? [
IconButton(
tooltip: '页面比例调节',
onPressed: () {
showDialog(
context: context,
builder: (context) => Align(
alignment: Alignment.topRight,
child: Container(
margin: EdgeInsets.only(
top: 56,
right: 16,
),
width: context.width / 4,
height: 32,
child: Builder(
builder: (context) => Slider(
min: 1,
max: 100,
value: _ratio.first,
onChanged: (value) async {
if (value >= 10 && value <= 90) {
_ratio[0] = value;
_ratio[1] = 100 - value;
await GStorage.setting.put(
SettingBoxKey.dynamicDetailRatio,
_ratio,
);
(context as Element).markNeedsBuild();
setState(() {});
}
},
),
),
),
),
);
},
icon: Transform.rotate(
angle: pi / 2,
child: Icon(Icons.splitscreen, size: 19),
),
),
const SizedBox(width: 16),
]
: null,
),
body: SafeArea(
top: false,
bottom: false,
child: context.orientation == Orientation.portrait
? refreshIndicator(
onRefresh: () async {
await _dynamicDetailController.onRefresh();
},
child: _buildBody(context.orientation, theme),
)
: _buildBody(context.orientation, theme),
),
);
}
Widget _buildBody(Orientation orientation, ThemeData theme) => Stack(
clipBehavior: Clip.none,
children: [
Builder(
builder: (context) {
double padding = max(context.width / 2 - Grid.smallCardWidth, 0);
if (orientation == Orientation.portrait) {
return CustomScrollView(
controller: _dynamicDetailController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: DynamicPanel(
item: _dynamicDetailController.item,
source: 'detail',
callback: _getImageCallback,
),
),
replyPersistentHeader(theme),
Obx(
() => replyList(
theme, _dynamicDetailController.loadingState.value),
),
]
.map<Widget>((e) => SliverPadding(
padding: EdgeInsets.symmetric(horizontal: padding),
sliver: e))
.toList(),
);
} else {
return Row(
children: [
Expanded(
flex: _ratio[0].toInt(),
child: CustomScrollView(
controller: _dynamicDetailController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
left: padding / 4,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: SliverToBoxAdapter(
child: DynamicPanel(
item: _dynamicDetailController.item,
source: 'detail',
callback: _getImageCallback,
),
),
),
],
),
),
Expanded(
flex: _ratio[1].toInt(),
child: Scaffold(
key: _key,
backgroundColor: Colors.transparent,
body: refreshIndicator(
onRefresh: () async {
await _dynamicDetailController.onRefresh();
},
child: CustomScrollView(
controller:
_dynamicDetailController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(right: padding / 4),
sliver: replyPersistentHeader(theme),
),
SliverPadding(
padding: EdgeInsets.only(right: padding / 4),
sliver: Obx(
() => replyList(
theme,
_dynamicDetailController
.loadingState.value),
),
),
],
),
),
),
),
],
);
}
},
),
if (_fabAnimationCtr != null)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: _fabAnimationCtr!,
curve: Curves.easeInOut,
)),
child: Builder(
builder: (context) {
Widget button() => FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
_dynamicDetailController.onReply(
context,
oid: _dynamicDetailController.oid,
replyType: ReplyType.values[replyType],
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
);
return _dynamicDetailController.showDynActionBar.not
? Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(
right: 14,
bottom:
MediaQuery.paddingOf(context).bottom + 14,
),
child: button(),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(
right: 14, bottom: 14),
child: button(),
),
Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border(
top: BorderSide(
color: theme.colorScheme.outline
.withOpacity(0.08),
),
),
),
padding: EdgeInsets.only(
bottom:
MediaQuery.paddingOf(context).bottom),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Builder(
builder: (btnContext) =>
TextButton.icon(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => RepostPanel(
item: _dynamicDetailController
.item,
callback: () {
int count =
_dynamicDetailController
.item
.modules
.moduleStat
?.forward
?.count ??
0;
_dynamicDetailController
.item
.modules
.moduleStat ??=
ModuleStatModel();
_dynamicDetailController
.item
.modules
.moduleStat
?.forward ??=
DynamicStat();
_dynamicDetailController
.item
.modules
.moduleStat!
.forward!
.count = count + 1;
if (btnContext.mounted) {
(btnContext as Element?)
?.markNeedsBuild();
}
},
),
);
},
icon: Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
color: theme.colorScheme.outline,
semanticLabel: "转发",
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(
15, 0, 15, 0),
foregroundColor:
theme.colorScheme.outline,
),
label: Text(
_dynamicDetailController
.item
.modules
.moduleStat
?.forward
?.count !=
null
? Utils.numFormat(
_dynamicDetailController
.item
.modules
.moduleStat!
.forward!
.count)
: '转发',
),
),
),
),
Expanded(
child: TextButton.icon(
onPressed: () {
Utils.shareText(
'${HttpString.dynamicShareBaseUrl}/${_dynamicDetailController.item.idStr}');
},
icon: Icon(
FontAwesomeIcons.shareNodes,
size: 16,
color: theme.colorScheme.outline,
semanticLabel: "分享",
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(
15, 0, 15, 0),
foregroundColor:
theme.colorScheme.outline,
),
label: const Text('分享'),
),
),
Expanded(
child: Builder(
builder: (context) => TextButton.icon(
onPressed: () =>
RequestUtils.onLikeDynamic(
_dynamicDetailController.item,
() {
if (context.mounted) {
(context as Element?)
?.markNeedsBuild();
}
},
),
icon: Icon(
_dynamicDetailController
.item
.modules
.moduleStat
?.like
?.status ==
true
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: _dynamicDetailController
.item
.modules
.moduleStat
?.like
?.status ==
true
? theme.colorScheme.primary
: theme.colorScheme.outline,
semanticLabel:
_dynamicDetailController
.item
.modules
.moduleStat
?.like
?.status ==
true
? "已赞"
: "点赞",
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(
15, 0, 15, 0),
foregroundColor:
theme.colorScheme.outline,
),
label: AnimatedSwitcher(
duration: const Duration(
milliseconds: 400),
transitionBuilder: (Widget child,
Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: child);
},
child: Text(
_dynamicDetailController
.item
.modules
.moduleStat
?.like
?.count !=
null
? Utils.numFormat(
_dynamicDetailController
.item
.modules
.moduleStat!
.like!
.count)
: '点赞',
style: TextStyle(
color: _dynamicDetailController
.item
.modules
.moduleStat
?.like
?.status ==
true
? theme.colorScheme.primary
: theme.colorScheme.outline,
),
),
),
),
),
),
],
),
),
],
);
},
),
),
),
],
);
SliverPersistentHeader replyPersistentHeader(ThemeData theme) {
return SliverPersistentHeader(
delegate: CustomSliverPersistentHeaderDelegate(
bgColor: theme.colorScheme.surface,
child: Container(
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.count.value != -1 ? _dynamicDetailController.count.value : 0}条回复',
key: ValueKey<int>(_dynamicDetailController.count.value),
),
),
),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () => _dynamicDetailController.queryBySort(),
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Obx(() => Text(
_dynamicDetailController.sortType.value.label,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
)),
),
)
],
),
),
),
pinned: true,
);
}
Widget replyList(
ThemeData theme, LoadingState<List<ReplyInfo>?> loadingState) {
return switch (loadingState) {
Loading() => SliverList.builder(
itemBuilder: (context, index) {
return const VideoReplySkeleton();
},
itemCount: 8,
),
Success() => loadingState.response?.isNotEmpty == true
? SliverList.builder(
itemBuilder: (context, index) {
if (index == loadingState.response!.length) {
_dynamicDetailController.onLoadMore();
return Container(
alignment: Alignment.center,
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
height: 125,
child: Text(
_dynamicDetailController.isEnd.not
? '加载中...'
: loadingState.response!.isEmpty
? '还没有评论'
: '没有更多了',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
);
} else {
return ReplyItemGrpc(
replyItem: loadingState.response![index],
replyLevel: '1',
replyReply: (replyItem, id) =>
replyReply(context, replyItem, id),
onReply: () {
_dynamicDetailController.onReply(
context,
replyItem: loadingState.response![index],
index: index,
);
},
onDelete: (subIndex) =>
_dynamicDetailController.onRemove(index, subIndex),
upMid: _dynamicDetailController.upMid,
callback: _getImageCallback,
onCheckReply: (item) =>
_dynamicDetailController.onCheckReply(context, item),
onToggleTop: (isUpTop, rpid) =>
_dynamicDetailController.onToggleTop(
index,
_dynamicDetailController.oid,
_dynamicDetailController.type,
isUpTop,
rpid,
),
);
}
},
itemCount: loadingState.response!.length + 1,
)
: HttpError(
onReload: _dynamicDetailController.onReload,
),
Error() => HttpError(
errMsg: loadingState.errMsg,
onReload: _dynamicDetailController.onReload,
),
};
}
}

View File

@@ -1,4 +0,0 @@
library dynamics;
export './controller.dart';
export './view.dart';

View File

@@ -1,373 +0,0 @@
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/emote/controller.dart';
import 'package:PiliPlus/pages/emote/view.dart';
import 'package:PiliPlus/pages/video/detail/reply_new/toolbar_icon_button.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class RepostPanel extends CommonPublishPage {
const RepostPanel({
super.key,
this.item,
this.dynIdStr,
this.callback,
// video
this.rid,
this.dynType,
this.pic,
this.title,
this.uname,
this.isMax,
});
// video
final int? rid;
final int? dynType;
final String? pic;
final String? title;
final String? uname;
final bool? isMax;
final DynamicItemModel? item;
final String? dynIdStr;
final VoidCallback? callback;
@override
State<RepostPanel> createState() => _RepostPanelState();
}
class _RepostPanelState extends CommonPublishPageState<RepostPanel> {
late bool _isMax = widget.isMax ?? false;
late final _pic = widget.pic ??
widget.item?.modules.moduleDynamic?.major?.archive?.cover ??
widget.item?.modules.moduleDynamic?.major?.pgc?.cover ??
widget.item?.modules.moduleDynamic?.major?.opus?.pics?.firstOrNull?.url;
late final _text = widget.title ??
widget.item?.modules.moduleDynamic?.major?.opus?.summary?.text ??
widget.item?.modules.moduleDynamic?.desc?.text ??
widget.item?.modules.moduleDynamic?.major?.archive?.title ??
widget.item?.modules.moduleDynamic?.major?.pgc?.title ??
'';
late final _uname = widget.uname ?? widget.item?.modules.moduleAuthor?.name;
@override
void dispose() {
try {
Get.delete<EmotePanelController>();
} catch (_) {}
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return AnimatedSize(
alignment: Alignment.topCenter,
curve: Curves.ease,
duration: const Duration(milliseconds: 300),
child: Column(
mainAxisSize: _isMax ? MainAxisSize.max : MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: _isMax ? 16 : 10),
_buildAppBar(theme),
if (_isMax)
Expanded(child: _buildEditPanel(theme))
else
_buildEditPanel(theme),
if (_isMax.not)
..._biuldDismiss(theme)
else ...[
_buildToolbar,
buildPanelContainer(Colors.transparent),
]
],
),
);
}
Widget _buildEditPanel(ThemeData theme) => Column(
mainAxisSize: _isMax ? MainAxisSize.max : MainAxisSize.min,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
width: double.infinity,
decoration: _isMax.not
? BoxDecoration(
border: Border(
left: BorderSide(
width: 2,
color: theme.colorScheme.primary,
),
),
)
: null,
child: _isMax.not
? _buildEditPlaceHolder(theme)
: _buildEditWidget,
),
),
),
const SizedBox(height: 10),
_buildRefWidget(theme),
],
);
Widget _buildRefWidget(ThemeData theme) => Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHigh ==
theme.colorScheme.surface
? theme.colorScheme.onInverseSurface
: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
if (_pic != null) ...[
NetworkImgLayer(
radius: 8,
width: 40,
height: 40,
src: _pic,
),
const SizedBox(width: 10),
],
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_uname?.isNotEmpty == true)
Text(
'@$_uname',
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 13,
),
),
Text(
_text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
Widget _buildEditPlaceHolder(ThemeData theme) => GestureDetector(
onTap: () async {
setState(() => _isMax = true);
await Future.delayed(const Duration(milliseconds: 300));
if (mounted) {
focusNode.requestFocus();
}
},
child: Text(
'说点什么吧',
style: TextStyle(
height: 1.75,
fontSize: 15,
color: theme.colorScheme.outline,
),
),
);
Widget get _buildEditWidget => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Listener(
onPointerUp: (event) {
if (readOnly.value) {
updatePanelType(PanelType.keyboard);
selectKeyboard.value = true;
}
},
child: Obx(
() => TextField(
controller: editController,
minLines: 4,
maxLines: null,
focusNode: focusNode,
readOnly: readOnly.value,
decoration: const InputDecoration(
hintText: '说点什么吧',
border: OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0,
),
contentPadding: EdgeInsets.symmetric(vertical: 10),
),
),
),
),
);
Widget _buildAppBar(ThemeData theme) => _isMax.not
? Row(
children: [
const SizedBox(width: 16),
Text(
widget.rid != null ? '分享至动态' : '转发动态',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Spacer(),
TextButton(
onPressed: onPublish,
style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
),
child: Text(widget.rid != null ? '立即发布' : '立即转发'),
),
const SizedBox(width: 16),
],
)
: Container(
height: 34,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 34,
height: 34,
child: IconButton(
tooltip: '返回',
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
backgroundColor:
WidgetStateProperty.resolveWith((states) {
return theme.colorScheme.secondaryContainer;
}),
),
onPressed: Get.back,
icon: Icon(
Icons.arrow_back_outlined,
size: 18,
color: theme.colorScheme.onSecondaryContainer,
),
),
),
),
Center(
child: Text(
widget.rid != null ? '分享至动态' : '转发动态',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
),
Align(
alignment: Alignment.centerRight,
child: FilledButton.tonal(
onPressed: onPublish,
style: FilledButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
),
child: Text(widget.rid != null ? '发布' : '转发'),
),
),
],
),
);
Widget get _buildToolbar => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Obx(
() => ToolbarIconButton(
onPressed: () {
selectKeyboard.value = PanelType.emoji == currentPanelType;
updatePanelType(
PanelType.emoji == currentPanelType
? PanelType.keyboard
: PanelType.emoji,
);
},
icon: const Icon(Icons.emoji_emotions, size: 22),
tooltip: '表情',
selected: !selectKeyboard.value,
),
),
],
),
);
List<Widget> _biuldDismiss(ThemeData theme) => [
const SizedBox(height: 10),
Divider(
height: 1,
color: theme.colorScheme.outline.withOpacity(0.1),
),
ListTile(
dense: true,
onTap: Get.back,
title: Center(
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
),
),
),
SizedBox(height: 10 + MediaQuery.of(context).padding.bottom),
];
@override
Widget? customPanel(double height) => SizedBox(
height: height,
child: EmotePanel(onChoose: onChooseEmote),
);
@override
Future onCustomPublish({required String message, List? pictures}) async {
dynamic result = await MsgHttp.createDynamic(
mid: Accounts.main.mid,
dynIdStr: widget.item?.idStr ?? widget.dynIdStr,
rid: widget.rid,
dynType: widget.dynType,
rawText: editController.text,
);
if (result['status']) {
Get.back();
SmartDialog.showToast('转发成功');
widget.callback?.call();
RequestUtils.insertCreatedDyn(result);
RequestUtils.checkCreatedDyn(
id: result['data']?['dyn_id'],
dynText: editController.text,
);
} else {
SmartDialog.showToast(result['msg']);
}
}
}

View File

@@ -1,81 +0,0 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/msg.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/pages/main/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import '../../../http/dynamics.dart';
class DynamicsTabController
extends CommonListController<DynamicsDataModel, DynamicItemModel> {
DynamicsTabController({required this.dynamicsType});
final String dynamicsType;
String offset = '';
int mid = -1;
late final MainController mainController = Get.find<MainController>();
@override
void onInit() {
super.onInit();
queryData();
}
@override
Future onRefresh() {
if (dynamicsType == 'all') {
mainController.setCount();
}
offset = '';
return super.onRefresh();
}
@override
List<DynamicItemModel>? getDataList(DynamicsDataModel response) {
return response.items;
}
@override
bool customHandleResponse(
bool isRefresh, Success<DynamicsDataModel> response) {
offset = response.response.offset ?? '';
return false;
}
@override
Future<LoadingState<DynamicsDataModel>> customGetData() =>
DynamicsHttp.followDynamic(
type: dynamicsType == "up" ? "all" : dynamicsType,
offset: offset,
mid: dynamicsType == "up" ? mid : -1,
);
Future onRemove(dynamic dynamicId) async {
var res = await MsgHttp.removeDynamic(dynIdStr: dynamicId);
if (res['status']) {
List<DynamicItemModel> list = (loadingState.value as Success).response;
list.removeWhere((item) => item.idStr == dynamicId);
loadingState.refresh();
SmartDialog.showToast('删除成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
Future onSetTop(bool isTop, dynamic dynamicId) async {
var res = await DynamicsHttp.setTop(dynamicId: dynamicId);
if (res['status']) {
SmartDialog.showToast('${isTop ? '取消' : ''}置顶成功');
} else {
SmartDialog.showToast(res['msg']);
}
}
@override
Future onReload() {
scrollController.jumpToTop();
return super.onReload();
}
}

View File

@@ -1,4 +0,0 @@
library dynamics.tab;
export './controller.dart';
export './view.dart';

View File

@@ -1,212 +0,0 @@
import 'dart:async';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/common_page.dart';
import 'package:PiliPlus/pages/main/controller.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/http_error.dart';
import 'package:waterfall_flow/waterfall_flow.dart';
import '../../../common/skeleton/dynamic_card.dart';
import '../../../utils/grid.dart';
import '../index.dart';
import '../widgets/dynamic_panel.dart';
import 'controller.dart';
class DynamicsTabPage extends CommonPage {
const DynamicsTabPage({super.key, required this.dynamicsType});
final String dynamicsType;
@override
State<DynamicsTabPage> createState() => _DynamicsTabPageState();
}
class _DynamicsTabPageState
extends CommonPageState<DynamicsTabPage, DynamicsTabController>
with AutomaticKeepAliveClientMixin {
late bool dynamicsWaterfallFlow;
StreamSubscription? _listener;
late final MainController _mainController = Get.find<MainController>();
DynamicsController dynamicsController = Get.put(DynamicsController());
@override
late DynamicsTabController controller = Get.put(
DynamicsTabController(dynamicsType: widget.dynamicsType)
..mid = dynamicsController.mid.value,
tag: widget.dynamicsType,
);
@override
bool get wantKeepAlive => true;
@override
void listener() {
if (_mainController.navigationBars[0]['id'] != 1 &&
_mainController.selectedIndex.value == 0) {
return;
}
super.listener();
}
@override
void initState() {
super.initState();
if (widget.dynamicsType == 'up') {
_listener = dynamicsController.mid.listen((mid) {
if (mid != -1) {
controller
..mid = mid
..onReload();
}
});
}
dynamicsWaterfallFlow = GStorage.setting
.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);
}
@override
void dispose() {
_listener?.cancel();
dynamicsController.mid.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return refreshIndicator(
onRefresh: () async {
dynamicsController.queryFollowUp();
await controller.onRefresh();
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: controller.scrollController,
slivers: [
Obx(() => _buildBody(controller.loadingState.value)),
],
),
);
}
Widget skeleton() {
if (!dynamicsWaterfallFlow) {
return SliverCrossAxisGroup(
slivers: [
const SliverFillRemaining(),
SliverConstrainedCrossAxis(
maxExtent: Grid.smallCardWidth * 2,
sliver: SliverList.builder(
itemBuilder: (context, index) {
return const DynamicCardSkeleton();
},
itemCount: 10,
),
),
const SliverFillRemaining()
],
);
}
return SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
crossAxisSpacing: StyleString.cardSpace / 2,
mainAxisSpacing: StyleString.cardSpace / 2,
maxCrossAxisExtent: Grid.smallCardWidth * 2,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent: 50,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return const DynamicCardSkeleton();
},
childCount: 10,
),
);
}
Widget _buildBody(LoadingState<List<DynamicItemModel>?> loadingState) {
return switch (loadingState) {
Loading() => skeleton(),
Success() => loadingState.response?.isNotEmpty == true
? SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: dynamicsWaterfallFlow
? SliverWaterfallFlow.extent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
crossAxisSpacing: StyleString.cardSpace / 2,
lastChildLayoutTypeBuilder: (index) {
if (index == loadingState.response!.length - 1) {
controller.onLoadMore();
}
return index == loadingState.response!.length
? LastChildLayoutType.foot
: LastChildLayoutType.none;
},
children: [
if (dynamicsController.tabController.index == 4 &&
dynamicsController.mid.value != -1) ...[
for (var i in loadingState.response!)
DynamicPanel(
item: i,
onRemove: controller.onRemove,
),
] else ...[
for (var i in loadingState.response!)
if (!dynamicsController.tempBannedList
.contains(i.modules.moduleAuthor?.mid))
DynamicPanel(
item: i,
onRemove: controller.onRemove,
),
]
],
)
: SliverCrossAxisGroup(
slivers: [
const SliverFillRemaining(),
SliverConstrainedCrossAxis(
maxExtent: Grid.smallCardWidth * 2,
sliver: SliverList.builder(
itemBuilder: (context, index) {
if (index == loadingState.response!.length - 1) {
controller.onLoadMore();
}
final item = loadingState.response![index];
if ((dynamicsController.tabController.index ==
4 &&
dynamicsController.mid.value != -1) ||
!dynamicsController.tempBannedList.contains(
item.modules.moduleAuthor?.mid)) {
return DynamicPanel(
item: item,
onRemove: controller.onRemove,
);
}
return const SizedBox.shrink();
},
itemCount: loadingState.response!.length,
),
),
const SliverFillRemaining(),
],
),
)
: HttpError(
onReload: controller.onReload,
),
Error() => HttpError(
errMsg: loadingState.errMsg,
onReload: controller.onReload,
),
};
}
}

View File

@@ -1,6 +1,6 @@
import 'package:PiliPlus/models/common/dynamics_type.dart';
import 'package:PiliPlus/models/common/up_panel_position.dart';
import 'package:PiliPlus/pages/dynamics/create_dyn_panel.dart';
import 'package:PiliPlus/pages/dynamics_create/view.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

View File

@@ -1,4 +1,4 @@
import 'package:PiliPlus/pages/dynamics/repost_dyn_panel.dart';
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart';

View File

@@ -1,10 +1,9 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/pic_panel.dart';
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/utils.dart';
import '../../../common/constants.dart';
import 'pic_panel.dart';
Widget articlePanel(
ThemeData theme,
String? source,

View File

@@ -2,9 +2,11 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/avatar.dart';
import 'package:PiliPlus/common/widgets/report.dart';
import 'package:PiliPlus/common/widgets/save_panel.dart';
import 'package:PiliPlus/pages/save_panel/view.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/request_utils.dart';
@@ -17,9 +19,6 @@ import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/utils.dart';
import '../../../http/constants.dart';
import '../controller.dart';
class AuthorPanel extends StatelessWidget {
final DynamicItemModel item;
final Function? addBannedList;

View File

@@ -3,7 +3,15 @@ import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/image_view.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/article/widgets/opus_content.dart';
import 'package:PiliPlus/pages/dynamics/widgets/additional_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/article_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/live_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/live_rcmd_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/pic_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
import 'package:PiliPlus/pages/dynamics/widgets/video_panel.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:flutter/material.dart';
@@ -11,15 +19,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/utils/utils.dart';
import '../../../models/dynamics/result.dart';
import 'additional_panel.dart';
import 'article_panel.dart';
import 'live_panel.dart';
import 'live_rcmd_panel.dart';
import 'pic_panel.dart';
import 'rich_node_panel.dart';
import 'video_panel.dart';
InlineSpan picsNodes(
List<OpusPicsModel> pics,
Function(List<String>, int)? callback,