mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-28 22:40:13 +08:00
tweaks (#1187)
* opt: marquee * fix: bangumi seek * opt: post panel * opt: remove deprecated code * opt: singleton dynController * fix: music scheme * feat: MemberVideo jump keep position * tweak
This commit is contained in:
committed by
GitHub
parent
e8a674ca2a
commit
172389b12b
@@ -56,9 +56,16 @@ class _AboutPageState extends State<AboutPage> {
|
||||
getCurrentApp();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
currentVersion.close();
|
||||
cacheSize.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> getCacheSize() async {
|
||||
cacheSize.value = CacheManage.formatSize(
|
||||
await CacheManage().loadApplicationCache(),
|
||||
await CacheManage.loadApplicationCache(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ class ArticleController extends CommonDynController {
|
||||
id = opusId;
|
||||
type = 'opus';
|
||||
}
|
||||
Get.putOrFind(() => this, tag: type + id);
|
||||
}
|
||||
init();
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/image_util.dart';
|
||||
import 'package:PiliPlus/utils/num_util.dart';
|
||||
@@ -42,9 +43,9 @@ class ArticlePage extends StatefulWidget {
|
||||
|
||||
class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
@override
|
||||
final ArticleController controller = Get.put(
|
||||
ArticleController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
final ArticleController controller = Get.putOrFind(
|
||||
ArticleController.new,
|
||||
tag: Get.parameters['type']! + Get.parameters['id']!,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -56,9 +57,9 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.scrollController.hasClients) {
|
||||
if (scrollController.hasClients) {
|
||||
controller.showTitle.value =
|
||||
controller.scrollController.positions.last.pixels >= 45;
|
||||
scrollController.positions.last.pixels >= 45;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -88,7 +89,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
_buildContent(
|
||||
@@ -117,7 +118,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
Expanded(
|
||||
flex: flex,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
@@ -150,7 +151,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
body: refreshIndicator(
|
||||
onRefresh: controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
buildReplyHeader(theme),
|
||||
@@ -554,7 +555,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: SlideTransition(
|
||||
position: controller.fabAnim,
|
||||
position: fabAnim,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (!controller.showDynActionBar) {
|
||||
|
||||
@@ -3,84 +3,17 @@ import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/common/reply_controller.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show ScrollDirection;
|
||||
import 'package:get/get.dart';
|
||||
|
||||
abstract class CommonDynController extends ReplyController<MainListReply>
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
abstract class CommonDynController extends ReplyController<MainListReply> {
|
||||
int get oid;
|
||||
int get replyType;
|
||||
|
||||
bool _showFab = true;
|
||||
late final AnimationController fabAnimationCtr;
|
||||
late final Animation<Offset> fabAnim;
|
||||
|
||||
late final RxBool showTitle = false.obs;
|
||||
|
||||
late final horizontalPreview = Pref.horizontalPreview;
|
||||
late final List<double> ratio = Pref.dynamicDetailRatio;
|
||||
|
||||
final fabOffset = const Offset(0, 1);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
fabAnim =
|
||||
Tween<Offset>(
|
||||
begin: fabOffset,
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: fabAnimationCtr,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
fabAnimationCtr.forward();
|
||||
scrollController.addListener(listener);
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void listener() {
|
||||
showTitle.value = scrollController.positions.first.pixels > 55;
|
||||
|
||||
final ScrollDirection direction1 =
|
||||
scrollController.positions.first.userScrollDirection;
|
||||
late final ScrollDirection direction2 =
|
||||
scrollController.positions.last.userScrollDirection;
|
||||
if (direction1 == ScrollDirection.forward ||
|
||||
direction2 == ScrollDirection.forward) {
|
||||
showFab();
|
||||
} else if (direction1 == ScrollDirection.reverse ||
|
||||
direction2 == ScrollDirection.reverse) {
|
||||
hideFab();
|
||||
}
|
||||
}
|
||||
|
||||
void showFab() {
|
||||
if (!_showFab) {
|
||||
_showFab = true;
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void hideFab() {
|
||||
if (_showFab) {
|
||||
_showFab = false;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
fabAnimationCtr.dispose();
|
||||
scrollController.removeListener(listener);
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList(
|
||||
type: replyType,
|
||||
|
||||
@@ -18,12 +18,15 @@ import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart' hide ContextExtensionss;
|
||||
|
||||
abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
with TickerProviderStateMixin {
|
||||
CommonDynController get controller;
|
||||
|
||||
late final scrollController = ScrollController()..addListener(listener);
|
||||
|
||||
late final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
bool get horizontalPreview => !isPortrait && controller.horizontalPreview;
|
||||
@@ -35,6 +38,49 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
late bool isPortrait;
|
||||
late double maxWidth;
|
||||
|
||||
bool _showFab = true;
|
||||
|
||||
final fabOffset = const Offset(0, 1);
|
||||
|
||||
late final AnimationController fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
)..forward();
|
||||
|
||||
late final Animation<Offset> fabAnim = Tween<Offset>(
|
||||
begin: fabOffset,
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(parent: fabAnimationCtr, curve: Curves.easeInOut));
|
||||
|
||||
void listener() {
|
||||
final pos = scrollController.positions;
|
||||
controller.showTitle.value = pos.first.pixels > 55;
|
||||
|
||||
final direction1 = pos.first.userScrollDirection;
|
||||
late final direction2 = pos.last.userScrollDirection;
|
||||
if (direction1 == ScrollDirection.forward ||
|
||||
direction2 == ScrollDirection.forward) {
|
||||
showFab();
|
||||
} else if (direction1 == ScrollDirection.reverse ||
|
||||
direction2 == ScrollDirection.reverse) {
|
||||
hideFab();
|
||||
}
|
||||
}
|
||||
|
||||
void showFab() {
|
||||
if (!_showFab) {
|
||||
_showFab = true;
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void hideFab() {
|
||||
if (_showFab) {
|
||||
_showFab = false;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -43,7 +89,7 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
isPortrait = size.isPortrait;
|
||||
imageCallback = horizontalPreview
|
||||
? (imgList, index) {
|
||||
controller.hideFab();
|
||||
hideFab();
|
||||
PageUtils.onHorizontalPreview(
|
||||
scaffoldKey,
|
||||
this,
|
||||
@@ -55,6 +101,12 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
padding = MediaQuery.viewPaddingOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget buildReplyHeader(ThemeData theme) {
|
||||
final secondary = theme.colorScheme.secondary;
|
||||
return SliverPersistentHeader(
|
||||
@@ -220,7 +272,7 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
} else {
|
||||
ScaffoldState? scaffoldState = Scaffold.maybeOf(context);
|
||||
if (scaffoldState != null) {
|
||||
controller.hideFab();
|
||||
hideFab();
|
||||
scaffoldState.showBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
(context) => replyReplyPage(showBackBtn: false),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class DynamicDetailController extends CommonDynController {
|
||||
@@ -30,11 +29,11 @@ class DynamicDetailController extends CommonDynController {
|
||||
_init(commentIdStr!, commentType);
|
||||
} else {
|
||||
DynamicsHttp.dynamicDetail(id: dynItem.idStr).then((res) {
|
||||
if (res['status']) {
|
||||
DynamicItemModel data = res['data'];
|
||||
if (res.isSuccess) {
|
||||
final data = res.data;
|
||||
_init(data.basic!.commentIdStr!, data.basic!.commentType!);
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
res.toast();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_detail/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/num_util.dart';
|
||||
import 'package:PiliPlus/utils/request_utils.dart';
|
||||
@@ -26,9 +27,9 @@ class DynamicDetailPage extends StatefulWidget {
|
||||
|
||||
class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
@override
|
||||
final DynamicDetailController controller = Get.put(
|
||||
DynamicDetailController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
final DynamicDetailController controller = Get.putOrFind(
|
||||
DynamicDetailController.new,
|
||||
tag: (Get.arguments['item'] as DynamicItemModel).idStr.toString(),
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -40,9 +41,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.scrollController.hasClients) {
|
||||
if (scrollController.hasClients) {
|
||||
controller.showTitle.value =
|
||||
controller.scrollController.positions.first.pixels > 55;
|
||||
scrollController.positions.first.pixels > 55;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -100,7 +101,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
child = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
@@ -126,7 +127,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
Expanded(
|
||||
flex: flex,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
@@ -161,7 +162,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
body: refreshIndicator(
|
||||
onRefresh: controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
buildReplyHeader(theme),
|
||||
@@ -192,7 +193,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: controller.fabAnim,
|
||||
position: fabAnim,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (!controller.showDynActionBar) {
|
||||
@@ -282,8 +283,8 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
int count = forward.count ?? 0;
|
||||
forward.count = count + 1;
|
||||
if (btnContext.mounted) {
|
||||
(btnContext as Element?)
|
||||
?.markNeedsBuild();
|
||||
(btnContext as Element)
|
||||
.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -315,7 +316,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
controller.dynItem,
|
||||
() {
|
||||
if (context.mounted) {
|
||||
(context as Element?)?.markNeedsBuild();
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -708,56 +708,59 @@ class LoginPageController extends GetxController
|
||||
};
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: const Text('选择账号mid, 为0时使用匿名'),
|
||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
||||
actionsPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: AccountType.values
|
||||
.map(
|
||||
(e) => WrapRadioOptionsGroup<Account>(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('选择账号mid, 为0时使用匿名'),
|
||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
||||
actionsPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: AccountType.values
|
||||
.map(
|
||||
(e) => Builder(
|
||||
builder: (context) => RadioGroup(
|
||||
groupValue: selectAccount[e],
|
||||
onChanged: (v) {
|
||||
selectAccount[e] = v!;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
child: WrapRadioOptionsGroup<Account>(
|
||||
groupTitle: e.title,
|
||||
options: options,
|
||||
selectedValue: selectAccount[e],
|
||||
onChanged: (v) => setState(() => selectAccount[e] = v!),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
for (var i in selectAccount.entries) {
|
||||
if (i.value != Accounts.get(i.key)) {
|
||||
Accounts.set(i.key, i.value);
|
||||
}
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
for (var i in selectAccount.entries) {
|
||||
if (i.value != Accounts.get(i.key)) {
|
||||
Accounts.set(i.key, i.value);
|
||||
}
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import 'package:PiliPlus/pages/match_info/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -31,14 +31,17 @@ class MatchInfoPage extends StatefulWidget {
|
||||
|
||||
class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
|
||||
@override
|
||||
final MatchInfoController controller = Get.put(
|
||||
MatchInfoController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
final MatchInfoController controller = Get.putOrFind(
|
||||
MatchInfoController.new,
|
||||
tag: Get.parameters['cid']!,
|
||||
);
|
||||
|
||||
@override
|
||||
dynamic get arguments => null;
|
||||
|
||||
@override
|
||||
Offset get fabOffset => const Offset(0, 2);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -48,7 +51,7 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
|
||||
body: refreshIndicator(
|
||||
onRefresh: controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
Obx(() => _buildInfo(theme, controller.infoState.value)),
|
||||
@@ -61,7 +64,7 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
|
||||
),
|
||||
),
|
||||
floatingActionButton: SlideTransition(
|
||||
position: controller.fabAnim,
|
||||
position: fabAnim,
|
||||
child: replyButton,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
|
||||
|
||||
@override
|
||||
bool customHandleResponse(bool isRefresh, Success<SpaceData> response) {
|
||||
SpaceData data = response.response;
|
||||
final data = response.response;
|
||||
username = data.card?.name ?? '';
|
||||
isFollowed = data.card?.relation?.isFollowed;
|
||||
if (data.relation == -1) {
|
||||
@@ -215,11 +215,7 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
|
||||
}
|
||||
|
||||
Future<void> onRemoveFan() async {
|
||||
final res = await VideoHttp.relationMod(
|
||||
mid: mid,
|
||||
act: 7,
|
||||
reSrc: 11,
|
||||
);
|
||||
final res = await VideoHttp.relationMod(mid: mid, act: 7, reSrc: 11);
|
||||
if (res['status']) {
|
||||
isFollowed = null;
|
||||
if (relation.value == 4) {
|
||||
|
||||
@@ -58,7 +58,6 @@ class _MemberPageState extends State<MemberPage> {
|
||||
if (_userController.loadingState.value.isSuccess) {
|
||||
return ExtendedNestedScrollView(
|
||||
key: _userController.key,
|
||||
controller: _userController.scrollController,
|
||||
onlyOneScrollInBody: true,
|
||||
pinnedHeaderSliverHeightBuilder: () =>
|
||||
kToolbarHeight + MediaQuery.viewPaddingOf(context).top,
|
||||
@@ -259,19 +258,10 @@ class _MemberPageState extends State<MemberPage> {
|
||||
] else ...[
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
content: MemberReportPanel(
|
||||
name: _userController.username,
|
||||
mid: _mid,
|
||||
),
|
||||
),
|
||||
onTap: () => showMemberReportDialog(
|
||||
context,
|
||||
name: _userController.username,
|
||||
mid: _mid,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -2,10 +2,13 @@ import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:PiliPlus/models/common/stat_type.dart';
|
||||
import 'package:PiliPlus/models_new/space/space_audio/item.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
|
||||
class MemberAudioItem extends StatelessWidget {
|
||||
const MemberAudioItem({super.key, required this.item});
|
||||
@@ -19,8 +22,18 @@ class MemberAudioItem extends StatelessWidget {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO
|
||||
onTap: () async {
|
||||
// TODO music play
|
||||
final aid = item.aid;
|
||||
if (aid != null) {
|
||||
final cid = await SearchHttp.ab2c(aid: aid);
|
||||
if (cid != null) {
|
||||
PageUtils.toVideoPage(cid: cid, aid: aid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
SmartDialog.showToast('没有MV');
|
||||
return;
|
||||
},
|
||||
onLongPress: () =>
|
||||
imageSaveDialog(title: item.title, cover: item.cover),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:PiliPlus/pages/member_contribute/controller.dart';
|
||||
import 'package:PiliPlus/pages/member_opus/view.dart';
|
||||
import 'package:PiliPlus/pages/member_season_series/view.dart';
|
||||
import 'package:PiliPlus/pages/member_video/view.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -31,8 +32,8 @@ class _MemberContributeState extends State<MemberContribute>
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
late final _controller = Get.put(
|
||||
MemberContributeCtr(
|
||||
late final _controller = Get.putOrFind(
|
||||
() => MemberContributeCtr(
|
||||
heroTag: widget.heroTag,
|
||||
initialIndex: widget.initialIndex,
|
||||
),
|
||||
|
||||
@@ -40,18 +40,19 @@ class MemberVideoCtr
|
||||
String? firstAid;
|
||||
String? lastAid;
|
||||
String? fromViewAid;
|
||||
Rx<bool?> isLocating = Rx<bool?>(null);
|
||||
bool? isLoadPrevious;
|
||||
RxBool isLocating = false.obs;
|
||||
bool isLoadPrevious = false;
|
||||
bool? hasPrev;
|
||||
|
||||
@override
|
||||
Future<void> onRefresh() async {
|
||||
if (isLocating.value == true) {
|
||||
if (isLocating.value) {
|
||||
if (hasPrev == true) {
|
||||
isLoadPrevious = true;
|
||||
await queryData();
|
||||
}
|
||||
} else {
|
||||
isLoadPrevious = false;
|
||||
firstAid = null;
|
||||
lastAid = null;
|
||||
next = null;
|
||||
@@ -76,15 +77,15 @@ class MemberVideoCtr
|
||||
bool isRefresh,
|
||||
Success<SpaceArchiveData> response,
|
||||
) {
|
||||
SpaceArchiveData data = response.response;
|
||||
final data = response.response;
|
||||
episodicButton
|
||||
..value = data.episodicButton ?? EpisodicButton()
|
||||
..refresh();
|
||||
next = data.next;
|
||||
if (page == 0 || isLoadPrevious == true) {
|
||||
if (page == 0 || isLoadPrevious) {
|
||||
hasPrev = data.hasPrev;
|
||||
}
|
||||
if (page == 0 || isLoadPrevious != true) {
|
||||
if (page == 0 || !isLoadPrevious) {
|
||||
if ((type == ContributeType.video
|
||||
? data.hasNext == false
|
||||
: data.next == 0) ||
|
||||
@@ -97,7 +98,7 @@ class MemberVideoCtr
|
||||
: (data.count ?? -1);
|
||||
if (page != 0 && loadingState.value.isSuccess) {
|
||||
data.item ??= <SpaceArchiveItem>[];
|
||||
if (isLoadPrevious == true) {
|
||||
if (isLoadPrevious) {
|
||||
data.item!.addAll(loadingState.value.data!);
|
||||
} else {
|
||||
data.item!.insertAll(0, loadingState.value.data!);
|
||||
@@ -105,7 +106,6 @@ class MemberVideoCtr
|
||||
}
|
||||
firstAid = data.item?.firstOrNull?.param;
|
||||
lastAid = data.item?.lastOrNull?.param;
|
||||
isLoadPrevious = null;
|
||||
loadingState.value = Success(data.item);
|
||||
return true;
|
||||
}
|
||||
@@ -116,13 +116,13 @@ class MemberVideoCtr
|
||||
type: type,
|
||||
mid: mid,
|
||||
aid: type == ContributeType.video
|
||||
? isLoadPrevious == true
|
||||
? isLoadPrevious
|
||||
? firstAid
|
||||
: lastAid
|
||||
: null,
|
||||
order: type == ContributeType.video ? order.value : null,
|
||||
sort: type == ContributeType.video
|
||||
? isLoadPrevious == true
|
||||
? isLoadPrevious
|
||||
? 'asc'
|
||||
: null
|
||||
: sort.value,
|
||||
@@ -130,12 +130,12 @@ class MemberVideoCtr
|
||||
next: next,
|
||||
seasonId: seasonId,
|
||||
seriesId: seriesId,
|
||||
includeCursor: isLocating.value == true && page == 0 ? true : null,
|
||||
includeCursor: isLocating.value && page == 0,
|
||||
);
|
||||
|
||||
void queryBySort() {
|
||||
if (type == ContributeType.video) {
|
||||
isLocating.value = null;
|
||||
isLocating.value = false;
|
||||
order.value = order.value == 'pubdate' ? 'click' : 'pubdate';
|
||||
} else {
|
||||
sort.value = sort.value == 'desc' ? 'asc' : 'desc';
|
||||
@@ -223,7 +223,7 @@ class MemberVideoCtr
|
||||
@override
|
||||
Future<void> onReload() {
|
||||
reload = true;
|
||||
isLocating.value = null;
|
||||
isLocating.value = false;
|
||||
return super.onReload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:PiliPlus/pages/member_video/controller.dart';
|
||||
import 'package:PiliPlus/pages/member_video/widgets/video_card_h_member_video.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MemberVideo extends StatefulWidget {
|
||||
@@ -59,8 +60,25 @@ class _MemberVideoState extends State<MemberVideo>
|
||||
super.build(context);
|
||||
final theme = Theme.of(context);
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
Widget child = refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
final child = refreshIndicator(
|
||||
onRefresh: () async {
|
||||
final count = _controller.loadingState.value.dataOrNull?.length;
|
||||
await _controller.onRefresh();
|
||||
if (_controller.isLoadPrevious) {
|
||||
if (mounted) {
|
||||
final newCount = _controller.loadingState.value.dataOrNull?.length;
|
||||
if (count != null && newCount != null && newCount > count) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
PrimaryScrollController.of(this.context).jumpTo(
|
||||
gridDelegate.layoutCache!
|
||||
.getGeometryForChildIndex(newCount - count)
|
||||
.scrollOffset,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: CustomScrollView(
|
||||
physics: ReloadScrollPhysics(controller: _controller),
|
||||
slivers: [
|
||||
@@ -80,23 +98,21 @@ class _MemberVideoState extends State<MemberVideo>
|
||||
children: [
|
||||
child,
|
||||
Obx(
|
||||
() => _controller.isLocating.value != true
|
||||
() => !_controller.isLocating.value
|
||||
? Positioned(
|
||||
right: 15 + padding.right,
|
||||
bottom: 15 + padding.bottom,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
final fromViewAid = _controller.fromViewAid;
|
||||
_controller
|
||||
..isLocating.value = true
|
||||
..lastAid = fromViewAid;
|
||||
final locatedIndex = _controller
|
||||
.loadingState
|
||||
.value
|
||||
.dataOrNull
|
||||
?.indexWhere((i) => i.param == fromViewAid);
|
||||
if (locatedIndex == null || locatedIndex == -1) {
|
||||
_controller.isLocating.value = true;
|
||||
final locatedIndex =
|
||||
_controller.loadingState.value.dataOrNull
|
||||
?.indexWhere((i) => i.param == fromViewAid) ??
|
||||
-1;
|
||||
if (locatedIndex == -1) {
|
||||
_controller
|
||||
..lastAid = fromViewAid
|
||||
..reload = true
|
||||
..page = 0
|
||||
..loadingState.value = LoadingState.loading()
|
||||
|
||||
@@ -20,6 +20,9 @@ class MusicDetailController extends CommonDynController {
|
||||
|
||||
bool get showDynActionBar => Pref.showDynActionBar;
|
||||
|
||||
String get shareUrl =>
|
||||
'https://music.bilibili.com/h5/music-detail?music_id=${musicId}';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:PiliPlus/models_new/music/bgm_recommend_list.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
typedef MusicRecommendArgs = ({String id, MusicDetail item});
|
||||
|
||||
class MusicRecommendController
|
||||
extends CommonListController<List<BgmRecommend>?, BgmRecommend> {
|
||||
late final String musicId;
|
||||
@@ -13,9 +15,9 @@ class MusicRecommendController
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final Map<String, dynamic> args = Get.arguments;
|
||||
musicId = args['id'];
|
||||
musicDetail = args['detail'];
|
||||
final MusicRecommendArgs args = Get.arguments;
|
||||
musicId = args.id;
|
||||
musicDetail = args.item;
|
||||
queryData();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_recommend_list.dart';
|
||||
import 'package:PiliPlus/pages/music/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/music/widget/music_video_card_h.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -20,9 +20,9 @@ class MusicRecommandPage extends StatefulWidget {
|
||||
|
||||
class _MusicRecommandPageState extends State<MusicRecommandPage>
|
||||
with GridMixin {
|
||||
late final _controller = Get.put(
|
||||
MusicRecommendController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
late final MusicRecommendController _controller = Get.putOrFind(
|
||||
MusicRecommendController.new,
|
||||
tag: (Get.arguments as MusicRecommendArgs).id,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -34,7 +34,6 @@ class _MusicRecommandPageState extends State<MusicRecommandPage>
|
||||
child: refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
_buildAppBar(theme, padding),
|
||||
|
||||
@@ -36,9 +36,9 @@ class MusicDetailPage extends StatefulWidget {
|
||||
|
||||
class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
@override
|
||||
final MusicDetailController controller = Get.put(
|
||||
MusicDetailController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
late final MusicDetailController controller = Get.putOrFind(
|
||||
MusicDetailController.new,
|
||||
tag: Get.parameters['musicId']!,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -110,7 +110,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
child = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
@@ -136,7 +136,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
Expanded(
|
||||
flex: flex,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
@@ -235,7 +235,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: controller.fabAnim,
|
||||
position: fabAnim,
|
||||
child: controller.showDynActionBar
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -288,9 +288,8 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
child: textIconButton(
|
||||
icon: CustomIcon.share_node,
|
||||
text: '分享',
|
||||
onPressed: () => Utils.shareText(
|
||||
'https://music.bilibili.com/h5/music-detail?music_id=${controller.musicId}',
|
||||
),
|
||||
onPressed: () =>
|
||||
Utils.shareText(controller.shareUrl),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -565,7 +564,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
theme,
|
||||
() => Get.to(
|
||||
const MusicRecommandPage(),
|
||||
arguments: {'id': controller.musicId, 'detail': item},
|
||||
arguments: (id: controller.musicId, item: item),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
import 'package:PiliPlus/models/common/video/video_type.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
|
||||
import 'package:PiliPlus/pages/music/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
@@ -74,8 +75,10 @@ class _SavePanelState extends State<SavePanel> {
|
||||
|
||||
//reply
|
||||
String? cover;
|
||||
_CoverType coverType = _CoverType.def16_9;
|
||||
String? title;
|
||||
int? pubdate;
|
||||
DateFormat dateFormat = DateUtil.longFormatDs;
|
||||
String? uname;
|
||||
|
||||
String uri = '';
|
||||
@@ -159,14 +162,42 @@ class _SavePanelState extends State<SavePanel> {
|
||||
} else if (currentRoute.startsWith('/articlePage')) {
|
||||
try {
|
||||
final type = reply.type.toInt();
|
||||
late final oid = reply.oid;
|
||||
late final rootId = hasRoot ? reply.root : reply.id;
|
||||
late final anchor = hasRoot ? 'anchor=${reply.id}&' : '';
|
||||
late final enterUri =
|
||||
final oid = reply.oid;
|
||||
final rootId = hasRoot ? reply.root : reply.id;
|
||||
final anchor = hasRoot ? 'anchor=${reply.id}&' : '';
|
||||
final enterUri =
|
||||
'bilibili://following/detail/${Get.parameters['id'] ?? Get.arguments?['id']}';
|
||||
uri =
|
||||
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri';
|
||||
} catch (_) {}
|
||||
} else if (currentRoute.startsWith('/musicDetail')) {
|
||||
final type = reply.type.toInt();
|
||||
final oid = reply.oid;
|
||||
final rootId = hasRoot ? reply.root : reply.id;
|
||||
final anchor = hasRoot ? 'anchor=${reply.id}&' : '';
|
||||
String enterUri = '';
|
||||
try {
|
||||
final ctr = Get.find<MusicDetailController>(
|
||||
tag: Get.parameters['musicId'],
|
||||
);
|
||||
// enterUri = 'enterUri=${Uri.encodeComponent(ctr.shareUrl)}'; // official client cannot parse it
|
||||
final data = ctr.infoState.value.dataOrNull;
|
||||
if (data != null) {
|
||||
coverType = _CoverType.square;
|
||||
cover = data.mvCover;
|
||||
title = data.musicTitle;
|
||||
if (data.musicPublish != null) {
|
||||
final time = DateTime.tryParse(
|
||||
data.musicPublish!,
|
||||
)?.millisecondsSinceEpoch;
|
||||
if (time != null) {
|
||||
pubdate = time ~/ 1000;
|
||||
dateFormat = DateUtil.longFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
uri = 'bilibili://comment/detail/$type/$oid/$rootId/?$anchor$enterUri';
|
||||
}
|
||||
|
||||
if (kDebugMode) debugPrint(uri);
|
||||
@@ -296,6 +327,7 @@ class _SavePanelState extends State<SavePanel> {
|
||||
final theme = Theme.of(context);
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
final maxWidth = context.mediaQueryShortestSide;
|
||||
late final coverSize = MediaQuery.textScalerOf(context).scale(65);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: Get.back,
|
||||
@@ -367,15 +399,10 @@ class _SavePanelState extends State<SavePanel> {
|
||||
NetworkImgLayer(
|
||||
radius: 6,
|
||||
src: cover!,
|
||||
height: MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(65),
|
||||
width:
|
||||
MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(65) *
|
||||
16 /
|
||||
9,
|
||||
height: coverSize,
|
||||
width: coverType == _CoverType.def16_9
|
||||
? coverSize * 16 / 9
|
||||
: coverSize,
|
||||
quality: 100,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -394,7 +421,7 @@ class _SavePanelState extends State<SavePanel> {
|
||||
Text(
|
||||
DateUtil.format(
|
||||
pubdate,
|
||||
format: DateUtil.longFormatDs,
|
||||
format: dateFormat,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
@@ -577,3 +604,5 @@ class _SavePanelState extends State<SavePanel> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _CoverType { def16_9, square }
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:math' show pi, max;
|
||||
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
|
||||
show ImageModel;
|
||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/fav.dart';
|
||||
@@ -25,7 +24,6 @@ import 'package:PiliPlus/pages/setting/widgets/slide_dialog.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/cache_manage.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:PiliPlus/utils/image_util.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
@@ -799,7 +797,7 @@ List<SettingsModel> get extraSettings => [
|
||||
final res = await FavHttp.allFavFolders(Accounts.main.mid);
|
||||
if (res.isSuccess) {
|
||||
final list = res.data.list;
|
||||
if (list.isNullOrEmpty) {
|
||||
if (list == null || list.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final quickFavId = Pref.quickFavId;
|
||||
@@ -809,22 +807,22 @@ List<SettingsModel> get extraSettings => [
|
||||
title: const Text('选择默认收藏夹'),
|
||||
contentPadding: const EdgeInsets.only(top: 5, bottom: 18),
|
||||
content: SingleChildScrollView(
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
children: List.generate(list!.length, (index) {
|
||||
final item = list[index];
|
||||
return RadioWidget(
|
||||
padding: const EdgeInsets.only(left: 14),
|
||||
title: item.title,
|
||||
groupValue: quickFavId,
|
||||
child: RadioGroup(
|
||||
onChanged: (value) {
|
||||
Get.back();
|
||||
GStorage.setting.put(SettingBoxKey.quickFavId, value);
|
||||
SmartDialog.showToast('设置成功');
|
||||
},
|
||||
groupValue: quickFavId,
|
||||
child: Column(
|
||||
children: list.map((item) {
|
||||
return RadioListTile(
|
||||
toggleable: true,
|
||||
dense: true,
|
||||
title: Text(item.title),
|
||||
value: item.id,
|
||||
onChanged: (value) {
|
||||
Get.back();
|
||||
GStorage.setting.put(SettingBoxKey.quickFavId, value);
|
||||
SmartDialog.showToast('设置成功');
|
||||
},
|
||||
);
|
||||
}),
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -577,7 +577,7 @@ List<SettingsModel> get styleSettings => [
|
||||
leading: const Icon(Icons.color_lens_outlined),
|
||||
title: '应用主题',
|
||||
getSubtitle: () =>
|
||||
'当前主题:${Get.put(ColorSelectController()).type.value == 0 ? '动态取色' : '指定颜色'}',
|
||||
'当前主题:${Get.put(ColorSelectController()).dynamicColor.value ? '动态取色' : '指定颜色'}',
|
||||
),
|
||||
SettingsModel(
|
||||
settingsType: SettingsType.normal,
|
||||
|
||||
@@ -98,13 +98,13 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
|
||||
),
|
||||
Obx(
|
||||
() => ListTile(
|
||||
enabled: ctr.type.value != 0,
|
||||
enabled: !ctr.dynamicColor.value,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('调色板风格'),
|
||||
PopupMenuButton(
|
||||
enabled: ctr.type.value != 0,
|
||||
enabled: !ctr.dynamicColor.value,
|
||||
initialValue: _dynamicSchemeVariant,
|
||||
onSelected: (item) {
|
||||
_dynamicSchemeVariant = item;
|
||||
@@ -130,7 +130,7 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 13,
|
||||
color: ctr.type.value == 0
|
||||
color: ctr.dynamicColor.value
|
||||
? theme.colorScheme.outline.withValues(
|
||||
alpha: 0.8,
|
||||
)
|
||||
@@ -141,7 +141,7 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: ctr.type.value == 0
|
||||
color: ctr.dynamicColor.value
|
||||
? theme.colorScheme.outline.withValues(
|
||||
alpha: 0.8,
|
||||
)
|
||||
@@ -164,27 +164,14 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => RadioListTile(
|
||||
value: 0,
|
||||
() => CheckboxListTile(
|
||||
title: const Text('动态取色'),
|
||||
groupValue: ctr.type.value,
|
||||
onChanged: (dynamic val) {
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: ctr.dynamicColor.value,
|
||||
onChanged: (val) {
|
||||
ctr
|
||||
..type.value = 0
|
||||
..setting.put(SettingBoxKey.dynamicColor, true);
|
||||
Get.forceAppUpdate();
|
||||
},
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => RadioListTile(
|
||||
value: 1,
|
||||
title: const Text('指定颜色'),
|
||||
groupValue: ctr.type.value,
|
||||
onChanged: (dynamic val) {
|
||||
ctr
|
||||
..type.value = 1
|
||||
..setting.put(SettingBoxKey.dynamicColor, false);
|
||||
..dynamicColor.value = val!
|
||||
..setting.put(SettingBoxKey.dynamicColor, val);
|
||||
Get.forceAppUpdate();
|
||||
},
|
||||
),
|
||||
@@ -196,78 +183,79 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
|
||||
alignment: Alignment.topCenter,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Obx(
|
||||
() => SizedBox(
|
||||
height: ctr.type.value == 0 ? 0 : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 22,
|
||||
runSpacing: 18,
|
||||
children: colorThemeTypes.indexed.map(
|
||||
(e) {
|
||||
final index = e.$1;
|
||||
final item = e.$2;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
ctr
|
||||
..currentColor.value = index
|
||||
..setting.put(SettingBoxKey.customColor, index);
|
||||
Get.forceAppUpdate();
|
||||
() => ctr.dynamicColor.value
|
||||
? const SizedBox.shrink(key: ValueKey(false))
|
||||
: Padding(
|
||||
key: const ValueKey(true),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 22,
|
||||
runSpacing: 18,
|
||||
children: colorThemeTypes.indexed.map(
|
||||
(e) {
|
||||
final index = e.$1;
|
||||
final item = e.$2;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
ctr
|
||||
..currentColor.value = index
|
||||
..setting.put(
|
||||
SettingBoxKey.customColor,
|
||||
index,
|
||||
);
|
||||
Get.forceAppUpdate();
|
||||
},
|
||||
child: Column(
|
||||
spacing: 3,
|
||||
children: [
|
||||
ColorPalette(
|
||||
color: item.color,
|
||||
selected: ctr.currentColor.value == index,
|
||||
),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ctr.currentColor.value != index
|
||||
? theme.colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
spacing: 3,
|
||||
children: [
|
||||
ColorPalette(
|
||||
color: item.color,
|
||||
selected: ctr.currentColor.value == index,
|
||||
),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ctr.currentColor.value != index
|
||||
? theme.colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...[
|
||||
Padding(
|
||||
padding: padding,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: size.height / 2,
|
||||
width: size.width,
|
||||
color: theme.colorScheme.surface,
|
||||
child: const HomePage(),
|
||||
),
|
||||
Padding(
|
||||
padding: padding,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: size.height / 2,
|
||||
width: size.width,
|
||||
color: theme.colorScheme.surface,
|
||||
child: const HomePage(),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: NavigationBar(
|
||||
destinations: NavigationBarType.values
|
||||
.map(
|
||||
(item) => NavigationDestination(
|
||||
icon: item.icon,
|
||||
label: item.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: NavigationBar(
|
||||
destinations: NavigationBarType.values
|
||||
.map(
|
||||
(item) => NavigationDestination(
|
||||
icon: item.icon,
|
||||
label: item.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -276,7 +264,6 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
|
||||
|
||||
class ColorSelectController extends GetxController {
|
||||
final RxBool dynamicColor = Pref.dynamicColor.obs;
|
||||
late final RxInt type = (dynamicColor.value ? 0 : 1).obs;
|
||||
final RxInt currentColor = Pref.customColor.obs;
|
||||
final RxDouble currentTextScale = Pref.defaultTextScale.obs;
|
||||
final Rx<ThemeType> themeType = Pref.themeType.obs;
|
||||
|
||||
@@ -75,28 +75,30 @@ class _SetDisplayModeState extends State<SetDisplayMode> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: modes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final DisplayMode mode = modes[index];
|
||||
return RadioListTile<DisplayMode>(
|
||||
value: mode,
|
||||
title: mode == DisplayMode.auto
|
||||
? const Text('自动')
|
||||
: Text('$mode${mode == active ? ' [系统]' : ''}'),
|
||||
groupValue: preferred,
|
||||
onChanged: (DisplayMode? newMode) {
|
||||
FlutterDisplayMode.setPreferredMode(
|
||||
newMode!,
|
||||
).whenComplete(
|
||||
() => Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
fetchAll,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: RadioGroup(
|
||||
onChanged: (DisplayMode? newMode) {
|
||||
FlutterDisplayMode.setPreferredMode(
|
||||
newMode!,
|
||||
).whenComplete(
|
||||
() => Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
fetchAll,
|
||||
),
|
||||
);
|
||||
},
|
||||
groupValue: preferred,
|
||||
child: ListView.builder(
|
||||
itemCount: modes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final DisplayMode mode = modes[index];
|
||||
return RadioListTile<DisplayMode>(
|
||||
value: mode,
|
||||
title: mode == DisplayMode.auto
|
||||
? const Text('自动')
|
||||
: Text('$mode${mode == active ? ' [系统]' : ''}'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -40,12 +40,12 @@ class _SlideColorPickerState extends State<SlideColorPicker> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _convert => Color.fromARGB(
|
||||
255,
|
||||
String get _convert => Color.fromRGBO(
|
||||
_r,
|
||||
_g,
|
||||
_b,
|
||||
).value.toRadixString(16).substring(2).toUpperCase();
|
||||
1,
|
||||
).toARGB32().toRadixString(16).substring(2).toUpperCase();
|
||||
|
||||
Widget _slider({
|
||||
required String title,
|
||||
|
||||
@@ -33,24 +33,26 @@ class SelectDialog<T> extends StatelessWidget {
|
||||
title: Text(title),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
values.length,
|
||||
(index) {
|
||||
final item = values[index];
|
||||
return RadioListTile<T>(
|
||||
dense: true,
|
||||
value: item.$1,
|
||||
title: Text(
|
||||
item.$2,
|
||||
style: titleMedium,
|
||||
),
|
||||
subtitle: subtitleBuilder?.call(context, index),
|
||||
groupValue: value,
|
||||
onChanged: Navigator.of(context).pop,
|
||||
);
|
||||
},
|
||||
child: RadioGroup<T>(
|
||||
onChanged: Navigator.of(context).pop,
|
||||
groupValue: value,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
values.length,
|
||||
(index) {
|
||||
final item = values[index];
|
||||
return RadioListTile<T>(
|
||||
dense: true,
|
||||
value: item.$1,
|
||||
title: Text(
|
||||
item.$2,
|
||||
style: titleMedium,
|
||||
),
|
||||
subtitle: subtitleBuilder?.call(context, index),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -423,7 +423,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
||||
setting.put(
|
||||
SettingBoxKey.blockColor,
|
||||
_blockColor
|
||||
.map((item) => item.value.toRadixString(16).substring(2))
|
||||
.map((item) => item.toARGB32().toRadixString(16).substring(2))
|
||||
.toList(),
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
|
||||
@@ -425,7 +425,7 @@ class VideoDetailController extends GetxController
|
||||
bool get showVideoSheet => !horizontalScreen && !isPortrait;
|
||||
|
||||
int? _lastPos;
|
||||
List<PostSegmentModel>? postList;
|
||||
List<PostSegmentModel> postList = [];
|
||||
RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
|
||||
List<Segment> viewPointList = <Segment>[];
|
||||
List<Segment>? segmentProgressList;
|
||||
@@ -1316,9 +1316,8 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
|
||||
void onBlock(BuildContext context) {
|
||||
postList ??= <PostSegmentModel>[];
|
||||
if (postList!.isEmpty) {
|
||||
postList!.add(
|
||||
if (postList.isEmpty) {
|
||||
postList.add(
|
||||
PostSegmentModel(
|
||||
segment: Pair(
|
||||
first: 0,
|
||||
|
||||
70
lib/pages/video/post_panel/popup_menu_text.dart
Normal file
70
lib/pages/video/post_panel/popup_menu_text.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
class PopupMenuText<T> extends StatefulWidget {
|
||||
final String title;
|
||||
final T initialValue;
|
||||
final PopupMenuItemSelected<T> onSelected;
|
||||
final PopupMenuItemBuilder<T> itemBuilder;
|
||||
final String Function(T) getSelectTitle;
|
||||
|
||||
const PopupMenuText({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.initialValue,
|
||||
required this.onSelected,
|
||||
required this.itemBuilder,
|
||||
required this.getSelectTitle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PopupMenuText<T>> createState() => _PopupMenuTextState();
|
||||
}
|
||||
|
||||
class _PopupMenuTextState<T> extends State<PopupMenuText<T>> {
|
||||
late T select = widget.initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('${widget.title}: '),
|
||||
PopupMenuButton<T>(
|
||||
initialValue: select,
|
||||
onSelected: (value) {
|
||||
if (value == select) return;
|
||||
setState(() {
|
||||
select = value;
|
||||
widget.onSelected(value);
|
||||
});
|
||||
},
|
||||
itemBuilder: widget.itemBuilder,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.getSelectTitle(select),
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
strutStyle: const StrutStyle(
|
||||
height: 1,
|
||||
leading: 0,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
MdiIcons.unfoldMoreHorizontal,
|
||||
size: MediaQuery.textScalerOf(context).scale(14),
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||
import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart';
|
||||
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
@@ -22,7 +23,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart' hide Response;
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
class PostPanel extends CommonCollapseSlidePage {
|
||||
const PostPanel({
|
||||
@@ -60,130 +60,122 @@ class PostPanel extends CommonCollapseSlidePage {
|
||||
required double currentPos,
|
||||
required double videoDuration,
|
||||
}) {
|
||||
List<Widget> segment(BuildContext context, bool isFirst) {
|
||||
String value = DurationUtil.formatDuration(
|
||||
isFirst ? item.segment.first : item.segment.second,
|
||||
);
|
||||
return [
|
||||
Text(
|
||||
'${isFirst ? '开始' : '结束'}: $value',
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '设为当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: currentPos,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: isFirst ? '视频开头' : '视频结尾',
|
||||
icon: isFirst ? Icons.first_page : Icons.last_page,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: isFirst ? 0 : videoDuration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '编辑',
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
showDialog<String>(
|
||||
Widget segment(bool isFirst) => Builder(
|
||||
builder: (context) {
|
||||
String value = DurationUtil.formatDuration(
|
||||
isFirst ? item.segment.first : item.segment.second,
|
||||
);
|
||||
return Row(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${isFirst ? '开始' : '结束'}: $value',
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
String initV = value;
|
||||
return AlertDialog(
|
||||
content: TextFormField(
|
||||
initialValue: value,
|
||||
autofocus: true,
|
||||
onChanged: (value) => initV = value,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[\d:.]+')),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: initV),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
size: 26,
|
||||
tooltip: '设为当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: currentPos,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
).then((res) {
|
||||
if (res != null) {
|
||||
try {
|
||||
List<num> split = res
|
||||
.split(':')
|
||||
.reversed
|
||||
.map(num.parse)
|
||||
.toList();
|
||||
double duration = 0;
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
duration += split[i] * pow(60, i);
|
||||
}
|
||||
if (duration <= videoDuration) {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: duration,
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: isFirst ? '视频开头' : '视频结尾',
|
||||
icon: isFirst ? Icons.first_page : Icons.last_page,
|
||||
onPressed: () {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: isFirst ? 0 : videoDuration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '编辑',
|
||||
icon: Icons.edit,
|
||||
onPressed: () async {
|
||||
final res = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
String initV = value;
|
||||
return AlertDialog(
|
||||
content: TextFormField(
|
||||
initialValue: value,
|
||||
autofocus: true,
|
||||
onChanged: (value) => initV = value,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[\d:.]+')),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: initV),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final child = Builder(
|
||||
builder: (context) => Row(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: segment(context, true),
|
||||
),
|
||||
if (res != null) {
|
||||
try {
|
||||
List<num> split = res
|
||||
.split(':')
|
||||
.reversed
|
||||
.map(num.parse)
|
||||
.toList();
|
||||
double duration = 0;
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
duration += split[i] * pow(60, i);
|
||||
}
|
||||
if (duration <= videoDuration) {
|
||||
updateSegment(
|
||||
isFirst: isFirst,
|
||||
item: item,
|
||||
value: duration,
|
||||
);
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (item.category != SegmentType.poi_highlight) {
|
||||
return Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
child,
|
||||
Builder(
|
||||
builder: (context) => Row(
|
||||
spacing: 5,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: segment(context, false),
|
||||
),
|
||||
),
|
||||
],
|
||||
children: [segment(true), segment(false)],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
return segment(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +183,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
late final VideoDetailController videoDetailController =
|
||||
widget.videoDetailController;
|
||||
late final PlPlayerController plPlayerController = widget.plPlayerController;
|
||||
late final List<PostSegmentModel>? list = videoDetailController.postList;
|
||||
late final List<PostSegmentModel> list = videoDetailController.postList;
|
||||
|
||||
late final double videoDuration =
|
||||
plPlayerController.durationSeconds.value.inMilliseconds / 1000;
|
||||
@@ -224,7 +216,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
tooltip: '添加片段',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
list?.insert(
|
||||
list.insert(
|
||||
0,
|
||||
PostSegmentModel(
|
||||
segment: Pair(
|
||||
@@ -263,18 +255,14 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
ListView.builder(
|
||||
controller: _controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.only(bottom: 88 + bottom),
|
||||
child: Column(
|
||||
children: List.generate(
|
||||
list!.length,
|
||||
(index) {
|
||||
return _buildItem(theme, index, list![index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(theme, index, list[index]);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
right: 16,
|
||||
@@ -294,10 +282,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_onPost();
|
||||
},
|
||||
onPressed: _onPost,
|
||||
child: const Text('确定提交'),
|
||||
),
|
||||
],
|
||||
@@ -310,59 +295,56 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onPost() {
|
||||
Request()
|
||||
.post(
|
||||
'${widget.videoDetailController.blockServer}/api/skipSegments',
|
||||
data: {
|
||||
'videoID': videoDetailController.bvid,
|
||||
'cid': videoDetailController.cid.value.toString(),
|
||||
'userID': Pref.blockUserID.toString(),
|
||||
'userAgent': Constants.userAgent,
|
||||
'videoDuration': videoDuration,
|
||||
'segments': list!
|
||||
.map(
|
||||
(item) => {
|
||||
'segment': [
|
||||
item.segment.first,
|
||||
item.segment.second,
|
||||
],
|
||||
'category': item.category.name,
|
||||
'actionType': item.actionType.name,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
options: Options(
|
||||
followRedirects: true, // Defaults to true.
|
||||
validateStatus: (int? status) {
|
||||
return (status! >= 200 && status < 300) ||
|
||||
const [400, 403, 429, 409] // reduce extra toast
|
||||
.contains(status);
|
||||
},
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(res) {
|
||||
if (res.statusCode == 200) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('提交成功');
|
||||
list?.clear();
|
||||
if (res.data case List list) {
|
||||
videoDetailController.handleSBData(
|
||||
list.map((e) => SegmentItemModel.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
plPlayerController.segmentList.value =
|
||||
videoDetailController.segmentProgressList ?? <Segment>[];
|
||||
if (videoDetailController.positionSubscription == null) {
|
||||
videoDetailController.initSkip();
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('提交失败: ${_errMsg(res)}');
|
||||
}
|
||||
},
|
||||
Future<void> _onPost() async {
|
||||
Get.back();
|
||||
final res = await Request().post(
|
||||
'${widget.videoDetailController.blockServer}/api/skipSegments',
|
||||
data: {
|
||||
'videoID': videoDetailController.bvid,
|
||||
'cid': videoDetailController.cid.value.toString(),
|
||||
'userID': Pref.blockUserID.toString(),
|
||||
'userAgent': Constants.userAgent,
|
||||
'videoDuration': videoDuration,
|
||||
'segments': list
|
||||
.map(
|
||||
(item) => {
|
||||
'segment': [
|
||||
item.segment.first,
|
||||
item.segment.second,
|
||||
],
|
||||
'category': item.category.name,
|
||||
'actionType': item.actionType.name,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
options: Options(
|
||||
followRedirects: true, // Defaults to true.
|
||||
validateStatus: (int? status) {
|
||||
return (status! >= 200 && status < 300) ||
|
||||
const [400, 403, 429, 409] // reduce extra toast
|
||||
.contains(status);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('提交成功');
|
||||
list.clear();
|
||||
if (res.data case List list) {
|
||||
videoDetailController.handleSBData(
|
||||
list.map((e) => SegmentItemModel.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
plPlayerController.segmentList.value =
|
||||
videoDetailController.segmentProgressList ?? <Segment>[];
|
||||
if (videoDetailController.positionSubscription == null) {
|
||||
videoDetailController.initSkip();
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('提交失败: ${_errMsg(res)}');
|
||||
}
|
||||
}
|
||||
|
||||
String _errMsg(Response res) {
|
||||
@@ -381,216 +363,142 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
|
||||
}
|
||||
|
||||
Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (item.actionType != ActionType.full)
|
||||
PostPanel.segmentWidget(
|
||||
theme,
|
||||
item: item,
|
||||
currentPos: currentPos,
|
||||
videoDuration: videoDuration,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (item.actionType != ActionType.full)
|
||||
PostPanel.segmentWidget(
|
||||
theme,
|
||||
item: item,
|
||||
currentPos: currentPos,
|
||||
videoDuration: videoDuration,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('分类: '),
|
||||
PopupMenuButton<SegmentType>(
|
||||
initialValue: item.category,
|
||||
onSelected: (e) {
|
||||
item.category = e;
|
||||
List<ActionType> constraintList = e.toActionType;
|
||||
if (!constraintList.contains(item.actionType)) {
|
||||
item.actionType = constraintList.first;
|
||||
}
|
||||
switch (e) {
|
||||
case SegmentType.poi_highlight:
|
||||
PostPanel.updateSegment(
|
||||
isFirst: false,
|
||||
item: item,
|
||||
value: item.segment.first,
|
||||
);
|
||||
break;
|
||||
case SegmentType.exclusive_access:
|
||||
PostPanel.updateSegment(
|
||||
isFirst: true,
|
||||
item: item,
|
||||
value: 0,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
itemBuilder: (context) => SegmentType.values
|
||||
.map(
|
||||
(e) => PopupMenuItem<SegmentType>(
|
||||
value: e,
|
||||
child: Text(e.title),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.category.title,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
strutStyle: const StrutStyle(
|
||||
height: 1,
|
||||
leading: 0,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
MdiIcons.unfoldMoreHorizontal,
|
||||
size: MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(14),
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
PopupMenuText(
|
||||
title: '分类',
|
||||
initialValue: item.category,
|
||||
onSelected: (e) {
|
||||
item.category = e;
|
||||
List<ActionType> constraintList = e.toActionType;
|
||||
if (!constraintList.contains(item.actionType)) {
|
||||
item.actionType = constraintList.first;
|
||||
}
|
||||
switch (e) {
|
||||
case SegmentType.poi_highlight:
|
||||
PostPanel.updateSegment(
|
||||
isFirst: false,
|
||||
item: item,
|
||||
value: item.segment.first,
|
||||
);
|
||||
break;
|
||||
case SegmentType.exclusive_access:
|
||||
PostPanel.updateSegment(
|
||||
isFirst: true,
|
||||
item: item,
|
||||
value: 0,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => SegmentType.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(value: e, child: Text(e.title)),
|
||||
)
|
||||
.toList(),
|
||||
getSelectTitle: (category) => category.title,
|
||||
),
|
||||
PopupMenuText(
|
||||
title: '行为类别',
|
||||
initialValue: item.actionType,
|
||||
onSelected: (e) {
|
||||
item.actionType = e;
|
||||
if (e == ActionType.full) {
|
||||
PostPanel.updateSegment(
|
||||
isFirst: true,
|
||||
item: item,
|
||||
value: 0,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => ActionType.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
enabled: item.category.toActionType.contains(e),
|
||||
value: e,
|
||||
child: Text(e.title),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('行为类别: '),
|
||||
PopupMenuButton<ActionType>(
|
||||
initialValue: item.actionType,
|
||||
onSelected: (e) {
|
||||
item.actionType = e;
|
||||
if (e == ActionType.full) {
|
||||
PostPanel.updateSegment(
|
||||
isFirst: true,
|
||||
item: item,
|
||||
value: 0,
|
||||
);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
itemBuilder: (context) => ActionType.values
|
||||
.map(
|
||||
(e) => PopupMenuItem<ActionType>(
|
||||
enabled: item.category.toActionType
|
||||
.contains(e),
|
||||
value: e,
|
||||
child: Text(e.title),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.actionType.title,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
strutStyle: const StrutStyle(
|
||||
height: 1,
|
||||
leading: 0,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
MdiIcons.unfoldMoreHorizontal,
|
||||
size: MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(14),
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
getSelectTitle: (i) => i.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 4,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '移除',
|
||||
icon: Icons.clear,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
list!.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 4,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '预览',
|
||||
icon: Icons.preview_outlined,
|
||||
onPressed: () async {
|
||||
if (widget.plPlayerController.videoPlayerController != null) {
|
||||
int start = max(
|
||||
0,
|
||||
(item.segment.first * 1000).round() - 2000,
|
||||
);
|
||||
await widget.plPlayerController.videoPlayerController!.seek(
|
||||
Duration(milliseconds: start),
|
||||
);
|
||||
if (!widget
|
||||
.plPlayerController
|
||||
.videoPlayerController!
|
||||
.state
|
||||
.playing) {
|
||||
await widget.plPlayerController.videoPlayerController!
|
||||
.play();
|
||||
}
|
||||
if (start != 0) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
}
|
||||
widget.plPlayerController.videoPlayerController!.seek(
|
||||
Duration(
|
||||
milliseconds: (item.segment.second * 1000).round(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 4,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '移除',
|
||||
icon: Icons.clear,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
list.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 4,
|
||||
child: iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '预览',
|
||||
icon: Icons.preview_outlined,
|
||||
onPressed: () async {
|
||||
final videoCtr = widget.plPlayerController.videoPlayerController;
|
||||
if (videoCtr != null) {
|
||||
final start = (item.segment.first * 1000).round();
|
||||
final seek = max(0, start - 2000);
|
||||
await videoCtr.seek(Duration(milliseconds: seek));
|
||||
if (!videoCtr.state.playing) {
|
||||
await videoCtr.play();
|
||||
}
|
||||
final delay = start - seek;
|
||||
if (delay > 0) {
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
}
|
||||
videoCtr.seek(
|
||||
Duration(milliseconds: (item.segment.second * 1000).round()),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
|
||||
msg: editController.text,
|
||||
mode: _mode.value,
|
||||
fontsize: _fontsize.value,
|
||||
color: isColorful ? null : _color.value.value & 0xFFFFFF,
|
||||
color: isColorful ? null : _color.value.toARGB32() & 0xFFFFFF,
|
||||
colorful: isColorful,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
|
||||
@@ -83,9 +83,7 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
Timer? clock;
|
||||
bool get isFullScreen => plPlayerController.isFullScreen.value;
|
||||
Box setting = GStorage.setting;
|
||||
MarqueeController? marqueeController;
|
||||
MarqueeController get _marqueeController =>
|
||||
marqueeController ??= MarqueeController(autoStart: false);
|
||||
late final provider = ContextSingleTicker(context);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -100,8 +98,6 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
@override
|
||||
void dispose() {
|
||||
clock?.cancel();
|
||||
marqueeController?.dispose();
|
||||
marqueeController = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1934,11 +1930,11 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
title,
|
||||
spacing: 30,
|
||||
velocity: 30,
|
||||
controller: _marqueeController,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
provider: provider,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:PiliPlus/models_new/msg/msg_dnd/uid_setting.dart';
|
||||
import 'package:PiliPlus/models_new/msg/session_ss/data.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -154,20 +153,9 @@ class WhisperLinkSettingController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void report() {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
content: MemberReportPanel(
|
||||
name: userState.value.dataOrNull?.firstOrNull?.name ?? '',
|
||||
mid: talkerUid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
void report() => showMemberReportDialog(
|
||||
Get.context!,
|
||||
name: userState.value.dataOrNull?.firstOrNull?.name,
|
||||
mid: talkerUid,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user