Merge remote-tracking branch 'upstream/main'

This commit is contained in:
orz12
2024-02-26 09:26:46 +08:00
40 changed files with 1929 additions and 357 deletions

View File

@@ -1,111 +0,0 @@
import 'dart:async';
import 'package:PiliPalaX/models/user/my_emote.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../../../../../common/widgets/network_img_layer.dart';
import '../../../../../http/reply.dart';
class EmoteTab extends StatefulWidget {
final Function(String) onEmoteTap;
const EmoteTab({Key? key, required this.onEmoteTap}) : super(key: key);
@override
State<StatefulWidget> createState() => _EmoteTabState();
}
class _EmoteTabState extends State<EmoteTab> with TickerProviderStateMixin {
late TabController _myEmoteTabController;
late MyEmote myEmote;
late Future futureBuild;
Future getMyEmote() async {
var result = await ReplyHttp.getMyEmote(business: "reply");
if (result['status']) {
myEmote = MyEmote.fromJson(result['data']);
_myEmoteTabController = TabController(
length: myEmote.packages!.length,
initialIndex: myEmote.setting!.focusPkgId! - 1,
vsync: this);
} else {
SmartDialog.showToast(result['msg']);
myEmote = MyEmote();
}
return;
}
@override
void initState() {
super.initState();
futureBuild = getMyEmote();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureBuild,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
myEmote != null &&
myEmote.packages != null) {
return Column(
children: [
Expanded(
child: TabBarView(controller: _myEmoteTabController, children: [
for (Packages i in myEmote.packages!) ...<Widget>[
GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: i.type == 4 ? 100 : 36,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
mainAxisExtent: 36,
),
itemCount: i.emote!.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
widget.onEmoteTap(i.emote![index].text!);
},
child: i.type == 4
? Text(
i.emote![index].text!,
overflow: TextOverflow.clip,
maxLines: 1,
)
: NetworkImgLayer(
width: 36,
height: 36,
type: 'emote',
src: i.emote![index].url?.split("@")[0]
),
);
},
),
],
]),
),
SizedBox(
height: 45,
child: TabBar(
isScrollable: true,
controller: _myEmoteTabController,
tabs: [
for (var i in myEmote.packages!)
NetworkImgLayer(
width: 36,
height: 36,
type: 'emote',
src: i.url,
),
],
))
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
class ToolbarIconButton extends StatelessWidget {
final VoidCallback onPressed;
final Icon icon;
final String toolbarType;
final bool selected;
const ToolbarIconButton({
super.key,
required this.onPressed,
required this.icon,
required this.toolbarType,
required this.selected,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 36,
height: 36,
child: IconButton(
onPressed: onPressed,
icon: icon,
highlightColor: Theme.of(context).colorScheme.secondaryContainer,
color: selected
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.outline,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith((states) {
return selected
? Theme.of(context).colorScheme.secondaryContainer
: null;
}),
),
),
);
}
}

View File

@@ -4,11 +4,12 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPalaX/http/video.dart';
import 'package:PiliPalaX/models/common/reply_type.dart';
import 'package:PiliPalaX/models/video/reply/emote.dart';
import 'package:PiliPalaX/models/video/reply/item.dart';
import 'package:PiliPalaX/pages/emote/index.dart';
import 'package:PiliPalaX/utils/feed_back.dart';
import '../../../../common/constants.dart';
import '../reply/reply_emote/view.dart';
import 'toolbar_icon_button.dart';
class VideoReplyNewDialog extends StatefulWidget {
final int? oid;
@@ -35,7 +36,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
final TextEditingController _replyContentController = TextEditingController();
final FocusNode replyContentFocusNode = FocusNode();
final GlobalKey _formKey = GlobalKey<FormState>();
bool isShowEmote = false;
late double emoteHeight = 0.0;
double keyboardHeight = 0.0; // 键盘高度
final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间
String toolbarType = 'input';
@override
void initState() {
@@ -46,6 +50,8 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
WidgetsBinding.instance.addObserver(this);
// 自动聚焦
_autoFocus();
// 监听聚焦状态
_focuslistener();
}
_autoFocus() async {
@@ -55,6 +61,16 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
}
}
_focuslistener() {
replyContentFocusNode.addListener(() {
if (replyContentFocusNode.hasFocus) {
setState(() {
toolbarType = 'input';
});
}
});
}
Future submitReplyAdd() async {
feedBack();
String message = _replyContentController.text;
@@ -77,18 +93,49 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
}
}
void onChooseEmote(Packages package, Emote emote) {
final int cursorPosition = _replyContentController.selection.baseOffset;
final String currentText = _replyContentController.text;
final String newText = currentText.substring(0, cursorPosition) +
emote.text! +
currentText.substring(cursorPosition);
_replyContentController.value = TextEditingValue(
text: newText,
selection:
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
);
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
if (keyboardHeight == 0 && emoteHeight == 0) {
setState(() {
emoteHeight = keyboardHeight =
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
});
}
}
});
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_replyContentController.dispose();
replyContentFocusNode.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
double keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom;
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
@@ -141,68 +188,32 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 36,
height: 36,
child: IconButton(
onPressed: () async {
FocusScope.of(context)
.requestFocus(replyContentFocusNode);
await Future.delayed(const Duration(milliseconds: 200));
ToolbarIconButton(
onPressed: () {
if (toolbarType == 'emote') {
setState(() {
isShowEmote = false;
toolbarType = 'input';
});
},
icon: Icon(Icons.keyboard,
size: 22,
color: Theme.of(context).colorScheme.onBackground),
highlightColor:
Theme.of(context).colorScheme.onInverseSurface,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.pressed) || !isShowEmote) {
return Theme.of(context).highlightColor;
}
// 默认状态下,返回透明颜色
return Colors.transparent;
}),
),
),
}
FocusScope.of(context).requestFocus(replyContentFocusNode);
},
icon: const Icon(Icons.keyboard, size: 22),
toolbarType: toolbarType,
selected: toolbarType == 'input',
),
const SizedBox(
width: 10,
),
SizedBox(
width: 36,
height: 36,
child: IconButton(
onPressed: () {
//收起输入法
FocusScope.of(context).unfocus();
// 弹出表情选择
const SizedBox(width: 20),
ToolbarIconButton(
onPressed: () {
if (toolbarType == 'input') {
setState(() {
isShowEmote = true;
toolbarType = 'emote';
});
},
icon: Icon(Icons.emoji_emotions,
size: 22,
color: Theme.of(context).colorScheme.onBackground),
highlightColor:
Theme.of(context).colorScheme.onInverseSurface,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.pressed) || isShowEmote) {
return Theme.of(context).highlightColor;
}
// 默认状态下,返回透明颜色
return Colors.transparent;
}),
),
),
}
FocusScope.of(context).unfocus();
},
icon: const Icon(Icons.emoji_emotions, size: 22),
toolbarType: toolbarType,
selected: toolbarType == 'emote',
),
const Spacer(),
TextButton(
@@ -210,42 +221,38 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
],
),
),
if (!isShowEmote)
SizedBox(
AnimatedSize(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300),
child: SizedBox(
width: double.infinity,
height: keyboardHeight,
),
if (isShowEmote)
SizedBox(
width: double.infinity,
height: 310,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace),
child: EmoteTab(
onEmoteTap: onEmoteTap,
),
height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
child: EmotePanel(
onChoose: (package, emote) => onChooseEmote(package, emote),
),
)
),
),
],
),
);
}
void onEmoteTap(String emoteString) {
// 在光标处插入表情
final String currentText = _replyContentController.text;
final TextSelection selection = _replyContentController.selection;
final String newText = currentText.replaceRange(
selection.start,
selection.end,
emoteString,
);
_replyContentController.text = newText;
final int newCursorIndex = selection.start + emoteString.length;
_replyContentController.selection = selection.copyWith(
baseOffset: newCursorIndex,
extentOffset: newCursorIndex,
);
}
}
typedef DebounceCallback = void Function();
class Debouncer {
DebounceCallback? callback;
final int? milliseconds;
Timer? _timer;
Debouncer({this.milliseconds});
run(DebounceCallback callback) {
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds!), () {
callback();
});
}
}

View File

@@ -19,6 +19,8 @@ import 'package:PiliPalaX/plugin/pl_player/models/play_repeat.dart';
import 'package:PiliPalaX/utils/storage.dart';
import 'package:PiliPalaX/http/danmaku.dart';
import 'package:PiliPalaX/services/shutdown_timer_service.dart';
import '../../../../models/video_detail_res.dart';
import '../introduction/index.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
const HeaderControl({
@@ -48,11 +50,31 @@ class _HeaderControlState extends State<HeaderControl> {
final Box<dynamic> videoStorage = GStrorage.video;
late List<double> speedsList;
double buttonSpace = 8;
bool showTitle = false;
late String heroTag;
late VideoIntroController videoIntroController;
late VideoDetailData videoDetail;
@override
void initState() {
super.initState();
videoInfo = widget.videoDetailCtr!.data;
speedsList = widget.controller!.speedsList;
fullScreenStatusListener();
heroTag = Get.arguments['heroTag'];
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
}
void fullScreenStatusListener() {
widget.videoDetailCtr!.plPlayerController.isFullScreen
.listen((bool isFullScreen) {
if (isFullScreen) {
showTitle = true;
} else {
showTitle = false;
}
setState(() {});
});
}
@override
@@ -1051,6 +1073,8 @@ class _HeaderControlState extends State<HeaderControl> {
color: Colors.white,
fontSize: 12,
);
final bool isLandscape =
MediaQuery.of(context).orientation == Orientation.landscape;
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
@@ -1085,21 +1109,47 @@ class _HeaderControlState extends State<HeaderControl> {
},
),
SizedBox(width: buttonSpace),
ComBtn(
icon: const Icon(
FontAwesomeIcons.house,
size: 15,
color: Colors.white,
if (showTitle && isLandscape) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 200),
child: Text(
videoIntroController.videoDetail.value.title!,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
if (videoIntroController.isShowOnlineTotal)
Text(
'${videoIntroController.total.value}人正在看',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
)
],
)
] else ...[
ComBtn(
icon: const Icon(
FontAwesomeIcons.house,
size: 15,
color: Colors.white,
),
fuc: () async {
// 销毁播放器实例
// await widget.controller!.dispose(type: 'all');
if (mounted) {
Navigator.popUntil(
context, (Route<dynamic> route) => route.isFirst);
}
},
),
fuc: () async {
// 销毁播放器实例
// await widget.controller!.dispose(type: 'all');
if (mounted) {
Navigator.popUntil(
context, (Route<dynamic> route) => route.isFirst);
}
},
),
],
const Spacer(),
// ComBtn(
// icon: const Icon(