Compare commits

...

5 Commits

Author SHA1 Message Date
dom
6755b7fae6 get real-time video duration in post panel
Closes #1901

Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 18:14:52 +08:00
dom
60801f9846 disable auto reset brightness on iOS
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 17:59:58 +08:00
dom
fae0cfe800 disable blank issue
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 16:44:56 +08:00
dom
dccb5d4bf5 create fav tag from fav panel
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 13:58:49 +08:00
dom
b9ce4bad67 opt follow item
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 13:58:49 +08:00
10 changed files with 200 additions and 146 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -593,7 +593,7 @@ abstract final class MemberHttp {
} }
} }
static Future<LoadingState<void>> createFollowTag(Object tagName) async { static Future<LoadingState<int>> createFollowTag(String tagName) async {
final res = await Request().post( final res = await Request().post(
Api.createFollowTag, Api.createFollowTag,
queryParameters: { queryParameters: {
@@ -607,7 +607,7 @@ abstract final class MemberHttp {
options: Options(contentType: Headers.formUrlEncodedContentType), options: Options(contentType: Headers.formUrlEncodedContentType),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return const Success(null); return Success(res.data['data']['tagid']);
} else { } else {
return Error(res.data['message']); return Error(res.data['message']);
} }

View File

@@ -42,6 +42,7 @@ import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart';
import 'package:window_manager/window_manager.dart' hide calcWindowPosition; import 'package:window_manager/window_manager.dart' hide calcWindowPosition;
WebViewEnvironment? webViewEnvironment; WebViewEnvironment? webViewEnvironment;
@@ -155,6 +156,8 @@ void main() async {
} }
FlutterDisplayMode.setPreferredMode(displayMode ?? DisplayMode.auto); FlutterDisplayMode.setPreferredMode(displayMode ?? DisplayMode.auto);
}); });
} else {
ScreenBrightnessPlatform.instance.setAutoReset(false);
} }
} else if (PlatformUtils.isDesktop) { } else if (PlatformUtils.isDesktop) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();

View File

@@ -17,4 +17,10 @@ class MemberTagItemModel {
tagid = json['tagid']; tagid = json['tagid'];
tip = json['tip']; tip = json['tip'];
} }
MemberTagItemModel.fromCreate(({int tagid, String tagName}) res) {
tagid = res.tagid;
name = res.tagName;
count = 0;
}
} }

View File

@@ -45,36 +45,35 @@ class FollowController extends GetxController with GetTickerProviderStateMixin {
tabs tabs
..assign(MemberTagItemModel(name: '全部关注')) ..assign(MemberTagItemModel(name: '全部关注'))
..addAll(response); ..addAll(response);
int initialIndex = 0; onInitTab();
if (tabController != null) {
initialIndex = tabController!.index.clamp(0, tabs.length - 1);
tabController!.dispose();
}
tabController = TabController(
initialIndex: initialIndex,
length: tabs.length,
vsync: this,
);
followState.value = Success(tabs.hashCode); followState.value = Success(tabs.hashCode);
} else { } else {
followState.value = res; followState.value = res;
} }
} }
@override void onInitTab() {
void onClose() { int initialIndex = 0;
tabController?.dispose(); if (tabController != null) {
super.onClose(); initialIndex = tabController!.index.clamp(0, tabs.length - 1);
tabController!.dispose();
}
tabController = TabController(
initialIndex: initialIndex,
length: tabs.length,
vsync: this,
);
} }
Future<void> onCreateTag(String tagName) async { void onCreateFavTag(({int tagid, String tagName}) res) {
final res = await MemberHttp.createFollowTag(tagName); if (isClosed) return;
if (res.isSuccess) { if (followState.value.isSuccess) {
tabs.add(MemberTagItemModel.fromCreate(res));
onInitTab();
followState.refresh();
} else {
followState.value = LoadingState.loading(); followState.value = LoadingState.loading();
queryFollowUpTags(); queryFollowUpTags();
SmartDialog.showToast('创建成功');
} else {
res.toast();
} }
} }
@@ -89,14 +88,22 @@ class FollowController extends GetxController with GetTickerProviderStateMixin {
} }
} }
Future<void> onDelTag(int tagid) async { Future<void> onDelTag(int index, int tagid) async {
final res = await MemberHttp.delFollowTag(tagid); final res = await MemberHttp.delFollowTag(tagid);
if (res.isSuccess) { if (res.isSuccess) {
followState.value = LoadingState.loading(); tabs.removeAt(index);
queryFollowUpTags(); onInitTab();
followState.refresh();
SmartDialog.showToast('删除成功'); SmartDialog.showToast('删除成功');
} else { } else {
res.toast(); res.toast();
} }
} }
@override
void onClose() {
tabController?.dispose();
tabController = null;
super.onClose();
}
} }

View File

@@ -8,6 +8,7 @@ import 'package:PiliPlus/pages/follow/child/child_controller.dart';
import 'package:PiliPlus/pages/follow/child/child_view.dart'; import 'package:PiliPlus/pages/follow/child/child_view.dart';
import 'package:PiliPlus/pages/follow/controller.dart'; import 'package:PiliPlus/pages/follow/controller.dart';
import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter;
@@ -45,57 +46,62 @@ class _FollowPageState extends State<FollowPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: _buildAppBar,
title: _followController.isOwner
? const Text('我的关注')
: Obx(() {
final name = _followController.name.value;
if (name != null) return Text('$name的关注');
return const SizedBox.shrink();
}),
actions: _followController.isOwner
? [
IconButton(
onPressed: _onCreateTag,
icon: const Icon(Icons.add),
tooltip: '新建分组',
),
IconButton(
onPressed: () => Get.toNamed(
'/followSearch',
arguments: {
'mid': _followController.mid,
},
),
icon: const Icon(Icons.search_outlined),
tooltip: '搜索',
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
onTap: () => Get.toNamed('/blackListPage'),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.block, size: 19),
SizedBox(width: 10),
Text('黑名单管理'),
],
),
),
],
),
const SizedBox(width: 6),
]
: null,
),
body: _followController.isOwner body: _followController.isOwner
? Obx(() => _buildBody(_followController.followState.value)) ? Obx(() => _buildBody(_followController.followState.value))
: _childPage(), : _childPage(),
); );
} }
PreferredSizeWidget get _buildAppBar => AppBar(
title: _followController.isOwner
? const Text('我的关注')
: Obx(() {
final name = _followController.name.value;
if (name != null) return Text('$name的关注');
return const SizedBox.shrink();
}),
actions: _followController.isOwner
? [
IconButton(
onPressed: () => RequestUtils.createFavTag(
context,
_followController.onCreateFavTag,
),
icon: const Icon(Icons.add),
tooltip: '新建分组',
),
IconButton(
onPressed: () => Get.toNamed(
'/followSearch',
arguments: {
'mid': _followController.mid,
},
),
icon: const Icon(Icons.search_outlined),
tooltip: '搜索',
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
onTap: () => Get.toNamed('/blackListPage'),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.block, size: 19),
SizedBox(width: 10),
Text('黑名单管理'),
],
),
),
],
),
const SizedBox(width: 6),
]
: null,
);
Widget _childPage([MemberTagItemModel? item]) => FollowChildPage( Widget _childPage([MemberTagItemModel? item]) => FollowChildPage(
tag: _tag, tag: _tag,
controller: _followController, controller: _followController,
@@ -223,7 +229,8 @@ class _FollowPageState extends State<FollowPage> {
context: context, context: context,
title: const Text('删除分组'), title: const Text('删除分组'),
content: const Text('删除后,该分组下的用户依旧保留?'), content: const Text('删除后,该分组下的用户依旧保留?'),
onConfirm: () => _followController.onDelTag(item.tagid!), onConfirm: () =>
_followController.onDelTag(index, item.tagid!),
); );
}, },
dense: true, dense: true,
@@ -237,22 +244,4 @@ class _FollowPageState extends State<FollowPage> {
), ),
); );
} }
void _onCreateTag() {
String tagName = '';
showConfirmDialog(
context: context,
title: const Text('新建分组'),
content: TextFormField(
autofocus: true,
initialValue: tagName,
onChanged: (value) => tagName = value,
inputFormatters: [
LengthLimitingTextInputFormatter(16),
],
decoration: const InputDecoration(border: OutlineInputBorder()),
),
onConfirm: () => _followController.onCreateTag(tagName),
);
}
} }

View File

@@ -8,7 +8,7 @@ import 'package:get/get.dart';
class FollowItem extends StatelessWidget { class FollowItem extends StatelessWidget {
final FollowItemModel item; final FollowItemModel item;
final bool? isOwner; final bool isOwner;
final ValueChanged? afterMod; final ValueChanged? afterMod;
final ValueChanged<UserModel>? onSelect; final ValueChanged<UserModel>? onSelect;
@@ -16,15 +16,38 @@ class FollowItem extends StatelessWidget {
super.key, super.key,
required this.item, required this.item,
this.afterMod, this.afterMod,
this.isOwner, bool? isOwner,
this.onSelect, this.onSelect,
}); }) : isOwner = isOwner ?? false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context); final colorScheme = ColorScheme.of(context);
Widget? followBtn;
if (isOwner) {
final isFollow = item.attribute != -1;
followBtn = FilledButton.tonal(
onPressed: () => RequestUtils.actionRelationMod(
context: context,
mid: item.mid,
isFollow: isFollow,
afterMod: afterMod,
),
style: FilledButton.styleFrom(
visualDensity: .compact,
tapTargetSize: .shrinkWrap,
padding: const .symmetric(horizontal: 15),
foregroundColor: isFollow ? colorScheme.outline : null,
backgroundColor: isFollow ? colorScheme.onInverseSurface : null,
),
child: Text(
'${isFollow ? '' : ''}关注',
style: const TextStyle(fontSize: 12),
),
);
}
return Material( return Material(
type: MaterialType.transparency, type: .transparency,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (onSelect != null) { if (onSelect != null) {
@@ -42,10 +65,7 @@ class FollowItem extends StatelessWidget {
} }
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const .symmetric(horizontal: 12, vertical: 10),
horizontal: 12,
vertical: 10,
),
child: Row( child: Row(
children: [ children: [
PendantAvatar( PendantAvatar(
@@ -58,19 +78,19 @@ class FollowItem extends StatelessWidget {
Expanded( Expanded(
child: Column( child: Column(
spacing: 3, spacing: 3,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
Text( Text(
item.uname!, item.uname!,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: .ellipsis,
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
if (item.sign != null) if (item.sign != null)
Text( Text(
item.sign!, item.sign!,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: .ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: colorScheme.outline, color: colorScheme.outline,
@@ -79,30 +99,7 @@ class FollowItem extends StatelessWidget {
], ],
), ),
), ),
if (isOwner ?? false) ?followBtn,
FilledButton.tonal(
onPressed: () => RequestUtils.actionRelationMod(
context: context,
mid: item.mid,
isFollow: item.attribute != -1,
afterMod: afterMod,
),
style: FilledButton.styleFrom(
visualDensity: .compact,
tapTargetSize: .shrinkWrap,
padding: const .symmetric(horizontal: 15),
foregroundColor: item.attribute == -1
? null
: colorScheme.outline,
backgroundColor: item.attribute == -1
? null
: colorScheme.onInverseSurface,
),
child: Text(
'${item.attribute == -1 ? '' : ''}关注',
style: const TextStyle(fontSize: 12),
),
),
], ],
), ),
), ),

View File

@@ -5,6 +5,7 @@ import 'package:PiliPlus/models/member/tags.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -26,7 +27,7 @@ class GroupPanel extends StatefulWidget {
class _GroupPanelState extends State<GroupPanel> { class _GroupPanelState extends State<GroupPanel> {
LoadingState<List<MemberTagItemModel>> loadingState = LoadingState.loading(); LoadingState<List<MemberTagItemModel>> loadingState = LoadingState.loading();
RxBool showDefaultBtn = true.obs; final RxBool showDefaultBtn = true.obs;
late final Set<int> tags = widget.tags == null late final Set<int> tags = widget.tags == null
? {} ? {}
: Set<int>.from(widget.tags!); : Set<int>.from(widget.tags!);
@@ -34,10 +35,10 @@ class _GroupPanelState extends State<GroupPanel> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_query(); _queryFollowUpTags();
} }
void _query() { void _queryFollowUpTags() {
MemberHttp.followUpTags().then((res) { MemberHttp.followUpTags().then((res) {
if (mounted) { if (mounted) {
loadingState = res..dataOrNull?.removeFirstWhere((e) => e.tagid == 0); loadingState = res..dataOrNull?.removeFirstWhere((e) => e.tagid == 0);
@@ -116,7 +117,7 @@ class _GroupPanelState extends State<GroupPanel> {
Error(:final errMsg) => scrollErrorWidget( Error(:final errMsg) => scrollErrorWidget(
controller: widget.scrollController, controller: widget.scrollController,
errMsg: errMsg, errMsg: errMsg,
onReload: _query, onReload: _queryFollowUpTags,
), ),
}; };
} }
@@ -125,8 +126,8 @@ class _GroupPanelState extends State<GroupPanel> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: .end,
children: <Widget>[ children: [
AppBar( AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
leading: IconButton( leading: IconButton(
@@ -135,6 +136,21 @@ class _GroupPanelState extends State<GroupPanel> {
icon: const Icon(Icons.close_outlined), icon: const Icon(Icons.close_outlined),
), ),
title: const Text('设置关注分组'), title: const Text('设置关注分组'),
actions: [
TextButton.icon(
onPressed: () =>
RequestUtils.createFavTag(context, _onCreateFavTag),
icon: Icon(Icons.add, color: theme.colorScheme.primary),
label: const Text('新建分组'),
style: const ButtonStyle(
visualDensity: .compact,
padding: WidgetStatePropertyAll(
.symmetric(horizontal: 18, vertical: 14),
),
),
),
const SizedBox(width: 16),
],
), ),
Expanded(child: _buildBody), Expanded(child: _buildBody),
Divider( Divider(
@@ -142,20 +158,28 @@ class _GroupPanelState extends State<GroupPanel> {
color: theme.disabledColor.withValues(alpha: 0.08), color: theme.disabledColor.withValues(alpha: 0.08),
), ),
Padding( Padding(
padding: EdgeInsets.only( padding: .only(
right: 20, right: 20,
top: 12, top: 12,
bottom: MediaQuery.viewPaddingOf(context).bottom + 12, bottom: MediaQuery.viewPaddingOf(context).bottom + 12,
), ),
child: FilledButton.tonal( child: FilledButton.tonal(
onPressed: onSave, onPressed: onSave,
style: FilledButton.styleFrom( style: const ButtonStyle(visualDensity: .compact),
visualDensity: VisualDensity.compact,
),
child: Obx(() => Text(showDefaultBtn.value ? '保存至默认分组' : '保存')), child: Obx(() => Text(showDefaultBtn.value ? '保存至默认分组' : '保存')),
), ),
), ),
], ],
); );
} }
void _onCreateFavTag(({int tagid, String tagName}) res) {
if (!mounted) return;
if (loadingState case Success(:final response)) {
response.add(MemberTagItemModel.fromCreate(res));
setState(() {});
} else {
_queryFollowUpTags();
}
}
} }

View File

@@ -183,7 +183,7 @@ class _PostPanelState extends State<PostPanel>
late final PlPlayerController plPlayerController = widget.plPlayerController; late final PlPlayerController plPlayerController = widget.plPlayerController;
late final List<PostSegmentModel> list = videoDetailController.postList; late final List<PostSegmentModel> list = videoDetailController.postList;
late final double videoDuration = double get videoDuration =>
plPlayerController.duration.value.inMilliseconds / 1000; plPlayerController.duration.value.inMilliseconds / 1000;
double currentPos() => plPlayerController.position.inMilliseconds / 1000; double currentPos() => plPlayerController.position.inMilliseconds / 1000;

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
@@ -37,6 +38,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
@@ -99,6 +101,35 @@ abstract final class RequestUtils {
} }
} }
static Future<void> createFavTag(
BuildContext context,
ValueChanged<({int tagid, String tagName})> onSuccess,
) async {
String tagName = '';
final onCreate = await showConfirmDialog(
context: context,
title: const Text('新建分组'),
content: TextFormField(
autofocus: true,
initialValue: tagName,
onChanged: (value) => tagName = value,
inputFormatters: [
LengthLimitingTextInputFormatter(16),
],
decoration: const InputDecoration(border: OutlineInputBorder()),
),
);
if (onCreate) {
final res = await MemberHttp.createFollowTag(tagName);
if (res case Success(:final response)) {
onSuccess((tagid: response, tagName: tagName));
SmartDialog.showToast('创建成功');
} else {
res.toast();
}
}
}
static Future<void> actionRelationMod({ static Future<void> actionRelationMod({
required BuildContext context, required BuildContext context,
required dynamic mid, required dynamic mid,
@@ -188,17 +219,13 @@ abstract final class RequestUtils {
expand: false, expand: false,
snapSizes: [maxChildSize], snapSizes: [maxChildSize],
initialChildSize: maxChildSize, initialChildSize: maxChildSize,
builder: builder: (context, scrollController) {
( return GroupPanel(
BuildContext context, mid: mid,
ScrollController scrollController, tags: followStatus!.tag,
) { scrollController: scrollController,
return GroupPanel( );
mid: mid, },
tags: followStatus!.tag,
scrollController: scrollController,
);
},
); );
}, },
); );