mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt pub page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
Binary file not shown.
@@ -9,19 +9,20 @@ class CustomIcon {
|
||||
static const IconData dm_settings = _CustomIconData(0xe803);
|
||||
static const IconData dyn = _CustomIconData(0xe804);
|
||||
static const IconData fav = _CustomIconData(0xe805);
|
||||
static const IconData share = _CustomIconData(0xe806);
|
||||
static const IconData share_line = _CustomIconData(0xe807);
|
||||
static const IconData share_node = _CustomIconData(0xe808);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe809);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80a);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe810);
|
||||
static const IconData topic_tag = _CustomIconData(0xe811);
|
||||
static const IconData watch_later = _CustomIconData(0xe812);
|
||||
static const IconData live_reserve = _CustomIconData(0xe806);
|
||||
static const IconData share = _CustomIconData(0xe807);
|
||||
static const IconData share_line = _CustomIconData(0xe808);
|
||||
static const IconData share_node = _CustomIconData(0xe809);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80a);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe811);
|
||||
static const IconData topic_tag = _CustomIconData(0xe812);
|
||||
static const IconData watch_later = _CustomIconData(0xe813);
|
||||
}
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
|
||||
@@ -26,7 +26,7 @@ import 'package:flutter/services.dart';
|
||||
/// created by bggRGjQaUbCoE on 2025/6/27
|
||||
///
|
||||
|
||||
enum RichTextType { text, composing, at, emoji, vote }
|
||||
enum RichTextType { text, composing, at, emoji, vote, common }
|
||||
|
||||
class Emote {
|
||||
late String url;
|
||||
@@ -752,7 +752,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
text: e.text,
|
||||
style: composingRegionOutOfRange ? null : composingStyle,
|
||||
);
|
||||
case RichTextType.at:
|
||||
case RichTextType.at || RichTextType.common:
|
||||
richStyle ??= (style ?? const TextStyle())
|
||||
.copyWith(color: Theme.of(context).colorScheme.primary);
|
||||
return TextSpan(
|
||||
|
||||
@@ -2305,18 +2305,18 @@ class RenderEditable extends RenderBox
|
||||
localPos: localPos,
|
||||
lastTapDownPosition: _lastTapDownPosition!,
|
||||
);
|
||||
position = TextPosition(offset: newOffset);
|
||||
final newSelection = TextSelection.collapsed(offset: newOffset);
|
||||
|
||||
final TextRange word = _textPainter.getWordBoundary(position);
|
||||
late TextSelection newSelection;
|
||||
if (position.offset <= word.start) {
|
||||
newSelection = TextSelection.collapsed(offset: word.start);
|
||||
} else {
|
||||
newSelection = TextSelection.collapsed(
|
||||
offset: word.end,
|
||||
affinity: TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
// final TextRange word = _textPainter.getWordBoundary(position);
|
||||
// late TextSelection newSelection;
|
||||
// if (position.offset <= word.start) {
|
||||
// newSelection = TextSelection.collapsed(offset: word.start);
|
||||
// } else {
|
||||
// newSelection = TextSelection.collapsed(
|
||||
// offset: word.end,
|
||||
// affinity: TextAffinity.upstream,
|
||||
// );
|
||||
// }
|
||||
_setSelection(newSelection, cause);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,4 +125,26 @@ class ReplyGrpc {
|
||||
);
|
||||
return res..dataOrNull?.replies.removeWhere((item) => needRemoveGrpc(item));
|
||||
}
|
||||
|
||||
static Future<LoadingState<SearchItemReply>> searchItem({
|
||||
required int page,
|
||||
required SearchItemType itemType,
|
||||
required int oid,
|
||||
int type = 1,
|
||||
String? keyword,
|
||||
}) {
|
||||
return GrpcReq.request(
|
||||
GrpcUrl.searchItem,
|
||||
SearchItemReq(
|
||||
cursor: SearchItemCursorReq(
|
||||
next: Int64(page),
|
||||
itemType: itemType,
|
||||
),
|
||||
oid: Int64(oid),
|
||||
type: Int64(type),
|
||||
keyword: keyword,
|
||||
),
|
||||
SearchItemReply.fromBuffer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class GrpcUrl {
|
||||
static const detailList = '$reply/DetailList';
|
||||
static const dialogList = '$reply/DialogList';
|
||||
// static const replyInfo = '$reply/ReplyInfo';
|
||||
static const searchItem = '$reply/SearchItem';
|
||||
|
||||
// im
|
||||
static const im = '/bilibili.im.interface.v1.ImInterface';
|
||||
|
||||
@@ -923,4 +923,10 @@ class Api {
|
||||
static const String createVote = '/x/vote/create';
|
||||
|
||||
static const String updateVote = '/x/vote/update';
|
||||
|
||||
static const String createReserve = '/x/new-reserve/up/reserve/create';
|
||||
|
||||
static const String updateReserve = '/x/new-reserve/up/reserve/update';
|
||||
|
||||
static const String reserveInfo = '/x/new-reserve/up/reserve/info';
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:PiliPlus/models_new/article/article_view/data.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_mention/data.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_reserve/data.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/topic_card_list.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
|
||||
@@ -131,6 +132,7 @@ class DynamicsHttp {
|
||||
List<Map<String, dynamic>>? extraContent,
|
||||
Pair<int, String>? topic,
|
||||
String? title,
|
||||
Map? attachCard,
|
||||
}) async {
|
||||
var res = await Request().post(
|
||||
Api.createDynamic,
|
||||
@@ -171,7 +173,7 @@ class DynamicsHttp {
|
||||
? 2
|
||||
: 1,
|
||||
if (pics != null) 'pics': pics,
|
||||
"attach_card": null,
|
||||
"attach_card": attachCard,
|
||||
"upload_id":
|
||||
"${rid != null ? 0 : mid}_${DateTime.now().millisecondsSinceEpoch ~/ 1000}_${Utils.random.nextInt(9000) + 1000}",
|
||||
"meta": {
|
||||
@@ -555,4 +557,72 @@ class DynamicsHttp {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<int?>> createReserve({
|
||||
int subType = 0,
|
||||
required String title,
|
||||
required int livePlanStartTime,
|
||||
}) async {
|
||||
final res = await Request().post(
|
||||
Api.createReserve,
|
||||
data: {
|
||||
'type': 2,
|
||||
'sub_type': subType,
|
||||
'from': 1,
|
||||
'title': title,
|
||||
'live_plan_start_time': livePlanStartTime,
|
||||
'csrf': Accounts.main.csrf,
|
||||
},
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(res.data['data']?['sid']);
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<int?>> updateReserve({
|
||||
int subType = 0,
|
||||
required String title,
|
||||
required int livePlanStartTime,
|
||||
required int sid,
|
||||
}) async {
|
||||
final res = await Request().post(
|
||||
Api.updateReserve,
|
||||
data: {
|
||||
'type': 2,
|
||||
'sub_type': subType,
|
||||
'from': 1,
|
||||
'title': title,
|
||||
'live_plan_start_time': livePlanStartTime,
|
||||
'id': sid,
|
||||
'csrf': Accounts.main.csrf,
|
||||
},
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(res.data['data']?['sid']);
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<ReserveInfoData>> reserveInfo({
|
||||
required dynamic sid,
|
||||
}) async {
|
||||
final res = await Request().get(
|
||||
Api.reserveInfo,
|
||||
queryParameters: {
|
||||
'from': 1,
|
||||
'id': sid,
|
||||
'web_location': 333.1365,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(ReserveInfoData.fromJson(res.data['data']));
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
enum PanelType { none, keyboard, emoji }
|
||||
enum PanelType { none, keyboard, emoji, more }
|
||||
|
||||
1
lib/models/common/reply/reply_search_type.dart
Normal file
1
lib/models/common/reply/reply_search_type.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum ReplySearchType { video, article }
|
||||
48
lib/models_new/dynamic/dyn_reserve_info/data.dart
Normal file
48
lib/models_new/dynamic/dyn_reserve_info/data.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
class ReserveInfoData {
|
||||
int? id;
|
||||
String? title;
|
||||
int? stime;
|
||||
int? etime;
|
||||
int? type;
|
||||
int? livePlanStartTime;
|
||||
int? lotteryType;
|
||||
String? lotteryId;
|
||||
int? subType;
|
||||
|
||||
ReserveInfoData({
|
||||
this.id,
|
||||
this.title,
|
||||
this.stime,
|
||||
this.etime,
|
||||
this.type,
|
||||
this.livePlanStartTime,
|
||||
this.lotteryType,
|
||||
this.lotteryId,
|
||||
this.subType,
|
||||
});
|
||||
|
||||
factory ReserveInfoData.fromJson(Map<String, dynamic> json) =>
|
||||
ReserveInfoData(
|
||||
id: json['id'] as int?,
|
||||
title: json['title'] as String?,
|
||||
stime: json['stime'] as int?,
|
||||
etime: json['etime'] as int?,
|
||||
type: json['type'] as int?,
|
||||
livePlanStartTime: json['live_plan_start_time'] as int?,
|
||||
lotteryType: json['lottery_type'] as int?,
|
||||
lotteryId: json['lottery_id'] as String?,
|
||||
subType: json['sub_type'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'stime': stime,
|
||||
'etime': etime,
|
||||
'type': type,
|
||||
'live_plan_start_time': livePlanStartTime,
|
||||
'lottery_type': lotteryType,
|
||||
'lottery_id': lotteryId,
|
||||
'sub_type': subType,
|
||||
};
|
||||
}
|
||||
@@ -107,13 +107,14 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
|
||||
void updatePanelType(PanelType type) {
|
||||
final isSwitchToKeyboard = PanelType.keyboard == type;
|
||||
final isSwitchToEmojiPanel = PanelType.emoji == type;
|
||||
final isSwitchToEmojiPanel =
|
||||
PanelType.emoji == type || PanelType.more == type;
|
||||
bool isUpdated = false;
|
||||
switch (type) {
|
||||
case PanelType.keyboard:
|
||||
updateInputView(isReadOnly: false);
|
||||
break;
|
||||
case PanelType.emoji:
|
||||
case PanelType.emoji || PanelType.more:
|
||||
isUpdated = updateInputView(isReadOnly: true);
|
||||
break;
|
||||
default:
|
||||
@@ -174,7 +175,9 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPanelContainer([Color? panelBgColor]) {
|
||||
Widget buildMorePanel(ThemeData theme) => throw UnimplementedError();
|
||||
|
||||
Widget buildPanelContainer(ThemeData theme, [Color? panelBgColor]) {
|
||||
return ChatBottomPanelContainer<PanelType>(
|
||||
controller: controller,
|
||||
inputFocusNode: focusNode,
|
||||
@@ -183,12 +186,13 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
switch (type) {
|
||||
case PanelType.emoji:
|
||||
return buildEmojiPickerPanel();
|
||||
case PanelType.more:
|
||||
return buildMorePanel(theme);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
onPanelTypeChange: (panelType, data) {
|
||||
// if (kDebugMode) debugPrint('panelType: $panelType');
|
||||
switch (panelType) {
|
||||
case ChatBottomPanelType.none:
|
||||
this.panelType.value = PanelType.none;
|
||||
@@ -198,14 +202,7 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
||||
break;
|
||||
case ChatBottomPanelType.other:
|
||||
if (data == null) return;
|
||||
switch (data) {
|
||||
case PanelType.emoji:
|
||||
this.panelType.value = PanelType.emoji;
|
||||
break;
|
||||
default:
|
||||
this.panelType.value = PanelType.none;
|
||||
break;
|
||||
}
|
||||
this.panelType.value = data;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_field.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/models/common/publish_panel_type.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_mention/item.dart';
|
||||
import 'package:PiliPlus/models_new/emote/emote.dart' as e;
|
||||
import 'package:PiliPlus/models_new/live/live_emote/emoticon.dart';
|
||||
@@ -14,6 +16,7 @@ import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
@@ -200,7 +203,7 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
final list = <Map<String, dynamic>>[];
|
||||
for (var e in editController.items) {
|
||||
switch (e.type) {
|
||||
case RichTextType.text || RichTextType.composing:
|
||||
case RichTextType.text || RichTextType.composing || RichTextType.common:
|
||||
list.add({
|
||||
"raw_text": e.text,
|
||||
"type": 1,
|
||||
@@ -360,4 +363,51 @@ abstract class CommonRichTextPubPageState<T extends CommonRichTextPubPage>
|
||||
void onSave() {
|
||||
widget.onSave?.call(editController.items);
|
||||
}
|
||||
|
||||
Widget get emojiBtn => Obx(
|
||||
() {
|
||||
final isEmoji = panelType.value == PanelType.emoji;
|
||||
return ToolbarIconButton(
|
||||
tooltip: isEmoji ? '输入' : '表情',
|
||||
onPressed: () {
|
||||
if (isEmoji) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
} else {
|
||||
updatePanelType(PanelType.emoji);
|
||||
}
|
||||
},
|
||||
icon: isEmoji
|
||||
? const Icon(Icons.keyboard, size: 22)
|
||||
: const Icon(Icons.emoji_emotions, size: 22),
|
||||
selected: isEmoji,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget get atBtn => ToolbarIconButton(
|
||||
onPressed: () => onMention(true),
|
||||
icon: const Icon(Icons.alternate_email, size: 22),
|
||||
tooltip: '@',
|
||||
selected: false,
|
||||
);
|
||||
|
||||
Widget get moreBtn => Obx(
|
||||
() {
|
||||
final isMore = panelType.value == PanelType.more;
|
||||
return ToolbarIconButton(
|
||||
tooltip: isMore ? '输入' : '更多',
|
||||
onPressed: () {
|
||||
if (isMore) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
} else {
|
||||
updatePanelType(PanelType.more);
|
||||
}
|
||||
},
|
||||
icon: isMore
|
||||
? const Icon(Icons.keyboard, size: 22)
|
||||
: const Icon(Icons.add_circle_outline, size: 22),
|
||||
selected: isMore,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ abstract class ReplyController<R> extends CommonListController<R, ReplyInfo> {
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
settings: RouteSettings(arguments: Get.arguments),
|
||||
),
|
||||
)
|
||||
.then(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math' show max;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart';
|
||||
@@ -11,8 +13,10 @@ import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/models/common/publish_panel_type.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
|
||||
import 'package:PiliPlus/models/dynamics/vote_model.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
|
||||
import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_reserve/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
|
||||
@@ -21,6 +25,7 @@ import 'package:PiliPlus/pages/emote/controller.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/request_utils.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
|
||||
@@ -68,6 +73,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
final Rx<ReplyOptionType> _replyOption = ReplyOptionType.allow.obs;
|
||||
final _titleEditCtr = TextEditingController();
|
||||
final Rx<Pair<int, String>?> topic = Rx<Pair<int, String>?>(null);
|
||||
final Rx<ReserveInfoData?> _reserveCard = Rx<ReserveInfoData?>(null);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -202,6 +208,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
child: _buildEditWidget(theme),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildReserveItem(theme),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
@@ -226,7 +233,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
),
|
||||
),
|
||||
_buildToolbar,
|
||||
buildPanelContainer(Colors.transparent),
|
||||
buildPanelContainer(theme, Colors.transparent),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -460,9 +467,10 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
),
|
||||
onPressed: _isPrivate.value
|
||||
? null
|
||||
: () {
|
||||
: () async {
|
||||
controller.keepChatPanel();
|
||||
DateTime nowDate = DateTime.now();
|
||||
showDatePicker(
|
||||
final selectedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: nowDate,
|
||||
firstDate: nowDate,
|
||||
@@ -471,45 +479,42 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
_publishTime.value = DateTime(
|
||||
selectedDate.year,
|
||||
selectedDate.month,
|
||||
selectedDate.day,
|
||||
selectedTime.hour,
|
||||
selectedTime.minute,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
if (selectedDate != null && mounted) {
|
||||
TimeOfDay nowTime = TimeOfDay.now();
|
||||
final selectedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: nowTime.replacing(
|
||||
hour: nowTime.minute + 6 >= 60
|
||||
? (nowTime.hour + 1) % 24
|
||||
: nowTime.hour,
|
||||
minute: (nowTime.minute + 6) % 60,
|
||||
),
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
_publishTime.value = DateTime(
|
||||
selectedDate.year,
|
||||
selectedDate.month,
|
||||
selectedDate.day,
|
||||
selectedTime.hour,
|
||||
selectedTime.minute,
|
||||
);
|
||||
}
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
child: const Text('定时发布'),
|
||||
)
|
||||
@@ -532,75 +537,10 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
child: Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
onPressed: () => updatePanelType(
|
||||
panelType.value == PanelType.emoji
|
||||
? PanelType.keyboard
|
||||
: PanelType.emoji,
|
||||
),
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
tooltip: '表情',
|
||||
selected: panelType.value == PanelType.emoji,
|
||||
),
|
||||
),
|
||||
ToolbarIconButton(
|
||||
onPressed: () => onMention(true),
|
||||
icon: const Icon(Icons.alternate_email, size: 22),
|
||||
tooltip: '@',
|
||||
selected: false,
|
||||
),
|
||||
ToolbarIconButton(
|
||||
onPressed: () async {
|
||||
controller.keepChatPanel();
|
||||
RichTextItem? voteItem = editController.items
|
||||
.firstWhereOrNull((e) => e.type == RichTextType.vote);
|
||||
VoteInfo? voteInfo = await Navigator.of(context).push(
|
||||
GetPageRoute(
|
||||
page: () => CreateVotePage(
|
||||
voteId: voteItem?.id == null
|
||||
? null
|
||||
: int.parse(voteItem!.id!))),
|
||||
);
|
||||
if (voteInfo != null) {
|
||||
if (voteItem != null) {
|
||||
final range = voteItem.range;
|
||||
final text = ' ${voteInfo.title} ';
|
||||
final selection = TextSelection.collapsed(
|
||||
offset: range.start + text.length);
|
||||
final delta = RichTextEditingDeltaReplacement(
|
||||
oldText: editController.text,
|
||||
replacementText: text,
|
||||
replacedRange: range,
|
||||
selection: selection,
|
||||
composing: TextRange.empty,
|
||||
type: RichTextType.vote,
|
||||
id: voteInfo.voteId.toString(),
|
||||
rawText: voteInfo.title,
|
||||
);
|
||||
final newValue = delta.apply(editController.value);
|
||||
editController
|
||||
..syncRichText(delta)
|
||||
..value = newValue;
|
||||
} else {
|
||||
onInsertText(
|
||||
'我发起了一个投票',
|
||||
RichTextType.text,
|
||||
);
|
||||
onInsertText(
|
||||
' ${voteInfo.title} ',
|
||||
RichTextType.vote,
|
||||
rawText: voteInfo.title,
|
||||
id: voteInfo.voteId.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
icon: const Icon(Icons.bar_chart_rounded, size: 24),
|
||||
tooltip: '投票',
|
||||
selected: false,
|
||||
),
|
||||
emojiBtn,
|
||||
atBtn,
|
||||
voteBtn,
|
||||
moreBtn,
|
||||
// if (kDebugMode)
|
||||
// ToolbarIconButton(
|
||||
// onPressed: editController.clear,
|
||||
@@ -611,6 +551,121 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget buildMorePanel(ThemeData theme) {
|
||||
double height = context.isTablet ? 300 : 170;
|
||||
final keyboardHeight = controller.keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
height = max(height, keyboardHeight);
|
||||
}
|
||||
|
||||
Widget item({
|
||||
required VoidCallback onTap,
|
||||
required Icon icon,
|
||||
required String title,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final color = theme.colorScheme.onSurfaceVariant;
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: GridView(
|
||||
padding: const EdgeInsets.only(left: 12, bottom: 12, right: 12),
|
||||
gridDelegate: const SliverGridDelegateWithExtentAndRatio(
|
||||
maxCrossAxisExtent: 65,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisExtent: 25,
|
||||
),
|
||||
children: [
|
||||
item(
|
||||
onTap: _onReserve,
|
||||
icon: Icon(CustomIcon.live_reserve, size: 28, color: color),
|
||||
title: '直播预约',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get voteBtn => ToolbarIconButton(
|
||||
onPressed: () async {
|
||||
controller.keepChatPanel();
|
||||
RichTextItem? voteItem = editController.items
|
||||
.firstWhereOrNull((e) => e.type == RichTextType.vote);
|
||||
VoteInfo? voteInfo = await Navigator.of(context).push(
|
||||
GetPageRoute(
|
||||
page: () => CreateVotePage(
|
||||
voteId: voteItem?.id == null
|
||||
? null
|
||||
: int.parse(voteItem!.id!))),
|
||||
);
|
||||
if (voteInfo != null) {
|
||||
if (voteItem != null) {
|
||||
final range = voteItem.range;
|
||||
final text = ' ${voteInfo.title} ';
|
||||
final selection =
|
||||
TextSelection.collapsed(offset: range.start + text.length);
|
||||
final delta = RichTextEditingDeltaReplacement(
|
||||
oldText: editController.text,
|
||||
replacementText: text,
|
||||
replacedRange: range,
|
||||
selection: selection,
|
||||
composing: TextRange.empty,
|
||||
type: RichTextType.vote,
|
||||
id: voteInfo.voteId.toString(),
|
||||
rawText: voteInfo.title,
|
||||
);
|
||||
final newValue = delta.apply(editController.value);
|
||||
editController
|
||||
..syncRichText(delta)
|
||||
..value = newValue;
|
||||
} else {
|
||||
onInsertText(
|
||||
'我发起了一个投票',
|
||||
RichTextType.text,
|
||||
);
|
||||
onInsertText(
|
||||
' ${voteInfo.title} ',
|
||||
RichTextType.vote,
|
||||
rawText: voteInfo.title,
|
||||
id: voteInfo.voteId.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
icon: const Icon(Icons.bar_chart_rounded, size: 24),
|
||||
tooltip: '投票',
|
||||
selected: false,
|
||||
);
|
||||
|
||||
Widget _buildEditWidget(ThemeData theme) => Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Listener(
|
||||
@@ -651,6 +706,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
SmartDialog.showLoading(msg: '正在发布');
|
||||
List<Map<String, dynamic>>? extraContent = getRichContent();
|
||||
final hasRichText = extraContent != null;
|
||||
final reserveCard = _reserveCard.value;
|
||||
var result = await DynamicsHttp.createDynamic(
|
||||
mid: Accounts.main.mid,
|
||||
rawText: hasRichText ? null : editController.text,
|
||||
@@ -663,6 +719,16 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
title: _titleEditCtr.text,
|
||||
topic: topic.value,
|
||||
extraContent: extraContent,
|
||||
attachCard: reserveCard == null
|
||||
? null
|
||||
: {
|
||||
"common_card": {
|
||||
"type": 14,
|
||||
"biz_id": reserveCard.id,
|
||||
"reserve_source": 0,
|
||||
"reserve_lottery": 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (result['status']) {
|
||||
@@ -682,18 +748,86 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
|
||||
}
|
||||
|
||||
double _topicOffset = 0;
|
||||
void _onSelectTopic() {
|
||||
SelectTopicPanel.onSelectTopic(
|
||||
Future<void> _onSelectTopic() async {
|
||||
controller.keepChatPanel();
|
||||
TopicItem? res = await SelectTopicPanel.onSelectTopic(
|
||||
context,
|
||||
offset: _topicOffset,
|
||||
callback: (offset) => _topicOffset = offset,
|
||||
).then((TopicItem? res) {
|
||||
if (res != null) {
|
||||
topic.value = Pair(first: res.id, second: res.name);
|
||||
}
|
||||
});
|
||||
);
|
||||
if (res != null) {
|
||||
topic.value = Pair(first: res.id, second: res.name);
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
}
|
||||
|
||||
@override
|
||||
void onSave() {}
|
||||
|
||||
Widget _buildReserveItem(ThemeData theme) {
|
||||
return Obx(
|
||||
() {
|
||||
final reserveCard = _reserveCard.value;
|
||||
if (reserveCard == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _onReserve,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 10),
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 30, 12),
|
||||
child: Column(
|
||||
spacing: 3,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('直播预约: ${reserveCard.title}'),
|
||||
Text(
|
||||
'${DateUtil.longFormatD.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
reserveCard.livePlanStartTime! * 1000),
|
||||
)} 直播',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 18,
|
||||
top: 2,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
size: 30,
|
||||
iconSize: 18,
|
||||
icon: Icons.clear,
|
||||
onPressed: () => _reserveCard.value = null,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onReserve() async {
|
||||
controller.keepChatPanel();
|
||||
ReserveInfoData? reserveInfo = await Navigator.of(context).push(
|
||||
GetPageRoute(
|
||||
page: () => CreateReservePage(sid: _reserveCard.value?.id),
|
||||
),
|
||||
);
|
||||
if (reserveInfo != null) {
|
||||
_reserveCard.value = reserveInfo;
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
}
|
||||
}
|
||||
|
||||
70
lib/pages/dynamics_create_reserve/controller.dart
Normal file
70
lib/pages/dynamics_create_reserve/controller.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateReserveController extends GetxController {
|
||||
CreateReserveController(this.sid);
|
||||
final int? sid;
|
||||
final RxInt subType = 0.obs;
|
||||
String key = Utils.generateRandomString(6);
|
||||
final RxString title = ''.obs;
|
||||
final now = DateTime.now();
|
||||
late final Rx<DateTime> date;
|
||||
late final end = now.copyWith(day: now.day + 90);
|
||||
final RxBool canCreate = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
date = DateTime(now.year, now.month, now.day + 1, 20, 0).obs;
|
||||
if (sid != null) {
|
||||
queryData();
|
||||
}
|
||||
}
|
||||
|
||||
void updateCanCreate() {
|
||||
canCreate.value = title.value.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
Future<void> queryData() async {
|
||||
var res = await DynamicsHttp.reserveInfo(sid: sid);
|
||||
if (res.isSuccess) {
|
||||
ReserveInfoData data = res.data;
|
||||
key = Utils.generateRandomString(6);
|
||||
title.value = data.title!;
|
||||
date.value =
|
||||
DateTime.fromMillisecondsSinceEpoch(data.livePlanStartTime! * 1000);
|
||||
canCreate.value = true;
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onCreate() async {
|
||||
final livePlanStartTime = date.value.millisecondsSinceEpoch ~/ 1000;
|
||||
var res = sid == null
|
||||
? await DynamicsHttp.createReserve(
|
||||
title: title.value,
|
||||
subType: subType.value,
|
||||
livePlanStartTime: livePlanStartTime,
|
||||
)
|
||||
: await DynamicsHttp.updateReserve(
|
||||
sid: sid!,
|
||||
subType: subType.value,
|
||||
title: title.value,
|
||||
livePlanStartTime: livePlanStartTime,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
Get.back(
|
||||
result: ReserveInfoData(
|
||||
id: res.data,
|
||||
title: title.value,
|
||||
livePlanStartTime: livePlanStartTime,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
}
|
||||
201
lib/pages/dynamics_create_reserve/view.dart
Normal file
201
lib/pages/dynamics_create_reserve/view.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
import 'package:PiliPlus/pages/dynamics_create_reserve/controller.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show TextInputFormatter, LengthLimitingTextInputFormatter;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateReservePage extends StatefulWidget {
|
||||
const CreateReservePage({super.key, this.sid});
|
||||
|
||||
final int? sid;
|
||||
|
||||
@override
|
||||
State<CreateReservePage> createState() => _CreateReservePageState();
|
||||
}
|
||||
|
||||
class _CreateReservePageState extends State<CreateReservePage> {
|
||||
late final _controller = Get.put(CreateReserveController(widget.sid),
|
||||
tag: Utils.generateRandomString(6));
|
||||
late TextStyle _leadingStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
_leadingStyle = TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.9),
|
||||
);
|
||||
final padding = MediaQuery.paddingOf(context);
|
||||
final divider = [
|
||||
const SizedBox(height: 10),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
];
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('添加直播预约')),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
left: padding.left + 16,
|
||||
right: padding.right + 16,
|
||||
bottom: padding.bottom + 80,
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text('类型', style: _leadingStyle),
|
||||
),
|
||||
Obx(
|
||||
() => PopupMenuButton(
|
||||
requestFocus: false,
|
||||
initialValue: _controller.subType.value,
|
||||
onSelected: (value) => _controller.subType.value = value,
|
||||
itemBuilder: (context) {
|
||||
return const [
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
child: Text('公开直播'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Text('大航海直播'),
|
||||
),
|
||||
];
|
||||
},
|
||||
child:
|
||||
Text(_controller.subType.value == 0 ? '公开直播' : '大航海直播'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...divider,
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text('时间', style: _leadingStyle),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
DateTime? newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.date.value,
|
||||
firstDate: _controller.now,
|
||||
lastDate: _controller.end,
|
||||
);
|
||||
if (newDate != null && context.mounted) {
|
||||
TimeOfDay? newTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime:
|
||||
TimeOfDay.fromDateTime(_controller.date.value),
|
||||
);
|
||||
if (newTime != null) {
|
||||
final newEndtime = DateTime(
|
||||
newDate.year,
|
||||
newDate.month,
|
||||
newDate.day,
|
||||
newTime.hour,
|
||||
newTime.minute,
|
||||
);
|
||||
if (newEndtime.difference(DateTime.now()) >=
|
||||
const Duration(minutes: 5)) {
|
||||
_controller.date.value = newEndtime;
|
||||
} else {
|
||||
SmartDialog.showToast('至少选择5分钟之后');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Obx(
|
||||
() => Text(
|
||||
DateUtil.longFormatD.format(_controller.date.value)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...divider,
|
||||
Obx(
|
||||
() => _buildInput(
|
||||
theme,
|
||||
key: ValueKey(_controller.key),
|
||||
initialValue: _controller.title.value,
|
||||
onChanged: (value) => _controller
|
||||
..title.value = value
|
||||
..updateCanCreate(),
|
||||
desc: '标题',
|
||||
hintText: '请填写标题,最多14字',
|
||||
inputFormatters: [LengthLimitingTextInputFormatter(14)],
|
||||
),
|
||||
),
|
||||
...divider,
|
||||
const SizedBox(height: 25),
|
||||
Obx(() {
|
||||
return FilledButton.tonal(
|
||||
onPressed:
|
||||
_controller.canCreate.value ? _controller.onCreate : null,
|
||||
child: const Text('添加预约'),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInput(
|
||||
ThemeData theme, {
|
||||
Key? key,
|
||||
String? initialValue,
|
||||
required ValueChanged<String> onChanged,
|
||||
required String desc,
|
||||
String? hintText,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
}) {
|
||||
return Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text(
|
||||
desc,
|
||||
style: _leadingStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: hintText ?? desc,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
inputFormatters: inputFormatters,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/draggable_sheet/draggable_scrollable_sheet_dyn.dart'
|
||||
show DraggableScrollableSheet;
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
@@ -96,7 +95,7 @@ class _RepostPanelState extends CommonRichTextPubPageState<RepostPanel> {
|
||||
),
|
||||
),
|
||||
_buildToolbar,
|
||||
buildPanelContainer(Colors.transparent),
|
||||
buildPanelContainer(theme, Colors.transparent),
|
||||
] else ...[
|
||||
..._buildEditPanel(theme),
|
||||
..._biuldDismiss(theme),
|
||||
@@ -328,26 +327,8 @@ class _RepostPanelState extends CommonRichTextPubPageState<RepostPanel> {
|
||||
child: Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
onPressed: () {
|
||||
updatePanelType(
|
||||
panelType.value == PanelType.emoji
|
||||
? PanelType.keyboard
|
||||
: PanelType.emoji,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
tooltip: '表情',
|
||||
selected: panelType.value == PanelType.emoji,
|
||||
),
|
||||
),
|
||||
ToolbarIconButton(
|
||||
onPressed: () => onMention(true),
|
||||
icon: const Icon(Icons.alternate_email, size: 22),
|
||||
tooltip: '@',
|
||||
selected: false,
|
||||
),
|
||||
emojiBtn,
|
||||
atBtn,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_field.dart';
|
||||
import 'package:PiliPlus/http/live.dart';
|
||||
import 'package:PiliPlus/models/common/publish_panel_type.dart';
|
||||
@@ -66,7 +65,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<LiveSendDmPanel> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...buildInputView(theme),
|
||||
buildPanelContainer(Colors.transparent),
|
||||
buildPanelContainer(theme, Colors.transparent),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -132,31 +131,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<LiveSendDmPanel> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
tooltip: '输入',
|
||||
onPressed: () {
|
||||
if (panelType.value != PanelType.keyboard) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.keyboard, size: 22),
|
||||
selected: panelType.value == PanelType.keyboard,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
tooltip: '表情',
|
||||
onPressed: () {
|
||||
if (panelType.value != PanelType.emoji) {
|
||||
updatePanelType(PanelType.emoji);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
selected: panelType.value == PanelType.emoji,
|
||||
),
|
||||
),
|
||||
emojiBtn,
|
||||
const Spacer(),
|
||||
Obx(
|
||||
() => FilledButton.tonal(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' show max;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart'
|
||||
@@ -12,10 +14,16 @@ import 'package:PiliPlus/models/common/publish_panel_type.dart';
|
||||
import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
|
||||
import 'package:PiliPlus/pages/emote/view.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/view.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart' hide TextField;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class ReplyPage extends CommonRichTextPubPage {
|
||||
final int oid;
|
||||
@@ -44,31 +52,7 @@ class ReplyPage extends CommonRichTextPubPage {
|
||||
|
||||
class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
final RxBool _syncToDynamic = false.obs;
|
||||
|
||||
Widget get child => SafeArea(
|
||||
bottom: false,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
color: themeData.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...buildInputView(),
|
||||
buildImagePreview(),
|
||||
buildPanelContainer(Colors.transparent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final heroTag = Get.arguments?['heroTag'];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -90,6 +74,30 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = SafeArea(
|
||||
bottom: false,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
color: themeData.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...buildInputView(),
|
||||
buildImagePreview(),
|
||||
buildPanelContainer(themeData, Colors.transparent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return darkVideoPage ? Theme(data: themeData, child: child) : child;
|
||||
}
|
||||
|
||||
@@ -167,31 +175,7 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
tooltip: '输入',
|
||||
onPressed: () {
|
||||
if (panelType.value != PanelType.keyboard) {
|
||||
updatePanelType(PanelType.keyboard);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.keyboard, size: 22),
|
||||
selected: panelType.value == PanelType.keyboard,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Obx(
|
||||
() => ToolbarIconButton(
|
||||
tooltip: '表情',
|
||||
onPressed: () {
|
||||
if (panelType.value != PanelType.emoji) {
|
||||
updatePanelType(PanelType.emoji);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
selected: panelType.value == PanelType.emoji,
|
||||
),
|
||||
),
|
||||
emojiBtn,
|
||||
if (widget.root == 0) ...[
|
||||
const SizedBox(width: 8),
|
||||
ToolbarIconButton(
|
||||
@@ -202,12 +186,9 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
ToolbarIconButton(
|
||||
onPressed: () => onMention(true),
|
||||
icon: const Icon(Icons.alternate_email, size: 22),
|
||||
tooltip: '@',
|
||||
selected: false,
|
||||
),
|
||||
atBtn,
|
||||
const SizedBox(width: 8),
|
||||
moreBtn,
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Obx(
|
||||
@@ -264,6 +245,144 @@ class _ReplyPageState extends CommonRichTextPubPageState<ReplyPage> {
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMorePanel(ThemeData theme) {
|
||||
double height = context.isTablet ? 300 : 170;
|
||||
final keyboardHeight = controller.keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
height = max(height, keyboardHeight);
|
||||
}
|
||||
|
||||
Widget item({
|
||||
required VoidCallback onTap,
|
||||
required Icon icon,
|
||||
required String title,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: themeData.colorScheme.onInverseSurface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isRoot = widget.root == 0;
|
||||
final color = themeData.colorScheme.onSurfaceVariant;
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: GridView(
|
||||
padding: const EdgeInsets.only(left: 12, bottom: 12, right: 12),
|
||||
gridDelegate: const SliverGridDelegateWithExtentAndRatio(
|
||||
maxCrossAxisExtent: 65,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisExtent: 25,
|
||||
),
|
||||
children: [
|
||||
item(
|
||||
onTap: () async {
|
||||
controller.keepChatPanel();
|
||||
({String title, String url})? res = await Get.to(
|
||||
ReplySearchPage(type: widget.replyType, oid: widget.oid));
|
||||
if (res != null) {
|
||||
onInsertText(
|
||||
'${res.title} ',
|
||||
RichTextType.common,
|
||||
rawText: '${res.url} ',
|
||||
);
|
||||
}
|
||||
controller.restoreChatPanel();
|
||||
},
|
||||
icon: Icon(Icons.post_add, size: 28, color: color),
|
||||
title: '插入内容',
|
||||
),
|
||||
if (heroTag != null) ...[
|
||||
// if (isRoot)
|
||||
// item(
|
||||
// onTap: () {
|
||||
// Get.back();
|
||||
// try {
|
||||
// Get.find<VideoDetailController>(tag: heroTag)
|
||||
// .showNoteList(context);
|
||||
// } catch (e) {
|
||||
// debugPrint(e.toString());
|
||||
// }
|
||||
// },
|
||||
// icon: Icon(Icons.edit_note, size: 28, color: color),
|
||||
// title: '笔记',
|
||||
// ),
|
||||
item(
|
||||
onTap: () {
|
||||
try {
|
||||
final plPlayerController =
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
onInsertText(
|
||||
' ${DurationUtil.formatDuration((plPlayerController.playedTime ?? Duration.zero).inSeconds)} ',
|
||||
RichTextType.common,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.my_location, size: 28, color: color),
|
||||
title: '视频进度',
|
||||
),
|
||||
if (isRoot)
|
||||
item(
|
||||
onTap: () async {
|
||||
if (pathList.length >= limit) {
|
||||
SmartDialog.showToast('最多选择$limit张图片');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final plPlayerController =
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
final res = await plPlayerController
|
||||
.plPlayerController.videoPlayerController
|
||||
?.screenshot(format: 'image/png');
|
||||
if (res != null) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
File file = File(
|
||||
'${tempDir.path}/${Utils.generateRandomString(8)}.png');
|
||||
await file.writeAsBytes(res);
|
||||
pathList.add(file.path);
|
||||
} else {
|
||||
debugPrint('null screenshot');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.enhance_photo_translate_outlined,
|
||||
size: 28, color: color),
|
||||
title: '视频截图',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onCustomPublish({List? pictures}) async {
|
||||
Map<String, int> atNameToMid = {};
|
||||
|
||||
36
lib/pages/video/reply_search_item/child/controller.dart
Normal file
36
lib/pages/video/reply_search_item/child/controller.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show SearchItemReply, SearchItem, SearchItemType;
|
||||
import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/controller.dart';
|
||||
|
||||
class ReplySearchChildController
|
||||
extends CommonListController<SearchItemReply, SearchItem> {
|
||||
ReplySearchChildController(this.controller, this.searchType);
|
||||
|
||||
final ReplySearchController controller;
|
||||
final ReplySearchType searchType;
|
||||
|
||||
@override
|
||||
List<SearchItem>? getDataList(SearchItemReply response) {
|
||||
if (response.cursor.hasNext == false) {
|
||||
isEnd = true;
|
||||
}
|
||||
return response.items;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<SearchItemReply>> customGetData() {
|
||||
return ReplyGrpc.searchItem(
|
||||
page: page,
|
||||
itemType: searchType == ReplySearchType.video
|
||||
? SearchItemType.VIDEO
|
||||
: SearchItemType.ARTICLE,
|
||||
oid: controller.oid,
|
||||
type: controller.type,
|
||||
keyword: controller.editingController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/pages/video/reply_search_item/child/view.dart
Normal file
94
lib/pages/video/reply_search_item/child/view.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show SearchItem;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/widgets/item.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchChildPage extends StatefulWidget {
|
||||
const ReplySearchChildPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.searchType,
|
||||
});
|
||||
|
||||
final ReplySearchChildController controller;
|
||||
final ReplySearchType searchType;
|
||||
|
||||
@override
|
||||
State<ReplySearchChildPage> createState() => _ReplySearchChildPageState();
|
||||
}
|
||||
|
||||
class _ReplySearchChildPageState extends State<ReplySearchChildPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
ReplySearchChildController get _controller => widget.controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildLoading {
|
||||
return SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
childCount: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<SearchItem>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => _buildLoading,
|
||||
Success(:var response) => response?.isNotEmpty == true
|
||||
? SliverGrid(
|
||||
gridDelegate: Grid.videoCardHDelegate(context),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == response.length - 1) {
|
||||
_controller.onLoadMore();
|
||||
}
|
||||
return ReplySearchItem(
|
||||
item: response[index],
|
||||
type: widget.searchType,
|
||||
);
|
||||
},
|
||||
childCount: response!.length,
|
||||
),
|
||||
)
|
||||
: HttpError(onReload: _controller.onReload),
|
||||
Error(:var errMsg) => HttpError(
|
||||
errMsg: errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
125
lib/pages/video/reply_search_item/child/widgets/item.dart
Normal file
125
lib/pages/video/reply_search_item/child/widgets/item.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show SearchItem, SearchItemVideoSubType;
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchItem extends StatelessWidget {
|
||||
const ReplySearchItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final SearchItem item;
|
||||
final ReplySearchType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String title = '';
|
||||
String cover = '';
|
||||
String? upNickname;
|
||||
String? category;
|
||||
int? duration;
|
||||
switch (type) {
|
||||
case ReplySearchType.video:
|
||||
if (item.video.type == SearchItemVideoSubType.UGC) {
|
||||
final ugc = item.video.ugc;
|
||||
title = ugc.title;
|
||||
cover = ugc.cover;
|
||||
upNickname = ugc.upNickname;
|
||||
duration = ugc.duration.toInt();
|
||||
} else {
|
||||
final pgc = item.video.pgc;
|
||||
title = pgc.title;
|
||||
cover = pgc.cover;
|
||||
category = pgc.category;
|
||||
}
|
||||
case ReplySearchType.article:
|
||||
final article = item.article;
|
||||
title = article.title;
|
||||
cover = article.covers.firstOrNull ?? '';
|
||||
upNickname = article.upNickname;
|
||||
}
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(result: (title: title, url: item.url)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: title,
|
||||
cover: cover,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: cover,
|
||||
width: boxConstraints.maxWidth,
|
||||
height: boxConstraints.maxHeight,
|
||||
),
|
||||
if (category != null)
|
||||
PBadge(
|
||||
right: 6,
|
||||
top: 6,
|
||||
text: category,
|
||||
),
|
||||
if (duration != null)
|
||||
PBadge(
|
||||
right: 6,
|
||||
bottom: 6,
|
||||
text: DurationUtil.formatDuration(duration),
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (upNickname != null)
|
||||
Text(
|
||||
'UP: $upNickname',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/pages/video/reply_search_item/controller.dart
Normal file
56
lib/pages/video/reply_search_item/controller.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/controller.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
ReplySearchController(this.type, this.oid);
|
||||
final int type;
|
||||
final int oid;
|
||||
|
||||
late final tabController = TabController(vsync: this, length: 2);
|
||||
final editingController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
late final videoCtr = Get.put(
|
||||
ReplySearchChildController(this, ReplySearchType.video),
|
||||
tag: Utils.generateRandomString(8));
|
||||
late final articleCtr = Get.put(
|
||||
ReplySearchChildController(this, ReplySearchType.article),
|
||||
tag: Utils.generateRandomString(8));
|
||||
|
||||
void onClear() {
|
||||
if (editingController.value.text.isNotEmpty) {
|
||||
editingController.clear();
|
||||
focusNode.requestFocus();
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
submit();
|
||||
}
|
||||
|
||||
void submit() {
|
||||
videoCtr
|
||||
..scrollController.jumpToTop()
|
||||
..onReload();
|
||||
articleCtr
|
||||
..scrollController.jumpToTop()
|
||||
..onReload();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
editingController.dispose();
|
||||
focusNode.dispose();
|
||||
tabController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
99
lib/pages/video/reply_search_item/view.dart
Normal file
99
lib/pages/video/reply_search_item/view.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/models/common/reply/reply_search_type.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/child/view.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_search_item/controller.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ReplySearchPage extends StatefulWidget {
|
||||
const ReplySearchPage({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.oid,
|
||||
});
|
||||
|
||||
final int type;
|
||||
final int oid;
|
||||
|
||||
@override
|
||||
State<ReplySearchPage> createState() => _ReplySearchPageState();
|
||||
}
|
||||
|
||||
class _ReplySearchPageState extends State<ReplySearchPage> {
|
||||
late final _controller = Get.put(
|
||||
ReplySearchController(widget.type, widget.oid),
|
||||
tag: Utils.generateRandomString(8));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '搜索',
|
||||
onPressed: _controller.submit,
|
||||
icon: const Icon(Icons.search, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _controller.focusNode,
|
||||
controller: _controller.editingController,
|
||||
textInputAction: TextInputAction.search,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索',
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: _controller.onClear,
|
||||
),
|
||||
),
|
||||
onSubmitted: (value) => _controller.submit(),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _controller.tabController,
|
||||
tabs: [
|
||||
const Tab(text: '视频'),
|
||||
const Tab(text: '专栏'),
|
||||
],
|
||||
onTap: (index) {
|
||||
if (!_controller.tabController.indexIsChanging) {
|
||||
if (index == 0) {
|
||||
_controller.videoCtr.animateToTop();
|
||||
} else {
|
||||
_controller.articleCtr.animateToTop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: tabBarView(
|
||||
controller: _controller.tabController,
|
||||
children: [
|
||||
ReplySearchChildPage(
|
||||
controller: _controller.videoCtr,
|
||||
searchType: ReplySearchType.video,
|
||||
),
|
||||
ReplySearchChildPage(
|
||||
controller: _controller.articleCtr,
|
||||
searchType: ReplySearchType.article,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -134,30 +134,6 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
),
|
||||
);
|
||||
|
||||
Widget get child => SafeArea(
|
||||
bottom: false,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 450),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
color: themeData.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildInputView(),
|
||||
buildPanelContainer(Colors.transparent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -170,6 +146,29 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = SafeArea(
|
||||
bottom: false,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 450),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
color: themeData.colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildInputView(),
|
||||
buildPanelContainer(themeData, Colors.transparent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return widget.darkVideoPage ? Theme(data: themeData, child: child) : child;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ class _WhisperDetailPageState
|
||||
),
|
||||
if (_whisperDetailController.mid != null) ...[
|
||||
_buildInputView(theme),
|
||||
buildPanelContainer(theme.colorScheme.onInverseSurface),
|
||||
buildPanelContainer(theme, theme.colorScheme.onInverseSurface),
|
||||
] else
|
||||
SizedBox(height: MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user