From 172389b12b664f1b73011738d10cbb6fd21484a5 Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:29:02 +0800 Subject: [PATCH] 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 --- lib/common/widgets/color_palette.dart | 2 +- lib/common/widgets/custom_toast.dart | 2 +- lib/common/widgets/dialog/report.dart | 153 ++-- lib/common/widgets/dialog/report_member.dart | 218 +++--- lib/common/widgets/marquee.dart | 149 ++-- lib/common/widgets/radio_widget.dart | 131 +++- lib/http/dynamics.dart | 17 +- lib/http/member.dart | 2 +- lib/main.dart | 55 +- lib/pages/about/view.dart | 9 +- lib/pages/article/controller.dart | 1 + lib/pages/article/view.dart | 19 +- .../common/dyn/common_dyn_controller.dart | 69 +- lib/pages/common/dyn/common_dyn_page.dart | 56 +- lib/pages/dynamics_detail/controller.dart | 7 +- lib/pages/dynamics_detail/view.dart | 25 +- lib/pages/login/controller.dart | 93 +-- lib/pages/match_info/view.dart | 15 +- lib/pages/member/controller.dart | 8 +- lib/pages/member/view.dart | 18 +- lib/pages/member_audio/widgets/item.dart | 17 +- lib/pages/member_contribute/view.dart | 5 +- lib/pages/member_video/controller.dart | 26 +- lib/pages/member_video/view.dart | 40 +- lib/pages/music/controller.dart | 3 + lib/pages/music/video/controller.dart | 8 +- lib/pages/music/video/view.dart | 9 +- lib/pages/music/view.dart | 19 +- lib/pages/save_panel/view.dart | 57 +- lib/pages/setting/models/extra_settings.dart | 32 +- lib/pages/setting/models/style_settings.dart | 2 +- lib/pages/setting/pages/color_select.dart | 165 ++--- lib/pages/setting/pages/display_mode.dart | 42 +- lib/pages/setting/slide_color_picker.dart | 6 +- lib/pages/setting/widgets/select_dialog.dart | 38 +- lib/pages/sponsor_block/view.dart | 2 +- lib/pages/video/controller.dart | 7 +- .../video/post_panel/popup_menu_text.dart | 70 ++ lib/pages/video/post_panel/view.dart | 678 ++++++++---------- lib/pages/video/send_danmaku/view.dart | 2 +- lib/pages/video/widgets/header_control.dart | 8 +- .../whisper_link_setting/controller.dart | 22 +- lib/plugin/pl_player/view.dart | 88 +-- lib/utils/app_scheme.dart | 56 +- lib/utils/cache_manage.dart | 33 +- lib/utils/context_ext.dart | 2 +- lib/utils/extension.dart | 5 + lib/utils/page_utils.dart | 10 +- lib/utils/request_utils.dart | 36 +- lib/utils/storage.dart | 2 +- lib/utils/storage_pref.dart | 2 +- 51 files changed, 1314 insertions(+), 1227 deletions(-) create mode 100644 lib/pages/video/post_panel/popup_menu_text.dart diff --git a/lib/common/widgets/color_palette.dart b/lib/common/widgets/color_palette.dart index 5008e67a..2be1b2ba 100644 --- a/lib/common/widgets/color_palette.dart +++ b/lib/common/widgets/color_palette.dart @@ -15,7 +15,7 @@ class ColorPalette extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final Hct hct = Hct.fromInt(color.value); + final Hct hct = Hct.fromInt(color.toARGB32()); final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt()); final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt()); final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt()); diff --git a/lib/common/widgets/custom_toast.dart b/lib/common/widgets/custom_toast.dart index 70e8c141..5659afa8 100644 --- a/lib/common/widgets/custom_toast.dart +++ b/lib/common/widgets/custom_toast.dart @@ -46,7 +46,7 @@ class LoadingWidget extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), decoration: BoxDecoration( - color: theme.dialogBackgroundColor, + color: theme.dialogTheme.backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(15)), ), child: Column( diff --git a/lib/common/widgets/dialog/report.dart b/lib/common/widgets/dialog/report.dart index 6da40b67..77bf0588 100644 --- a/lib/common/widgets/dialog/report.dart +++ b/lib/common/widgets/dialog/report.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -void autoWrapReportDialog( +Future autoWrapReportDialog( BuildContext context, Map> options, Future Function(int reasonType, String? reasonDesc, bool banUid) @@ -14,30 +14,30 @@ void autoWrapReportDialog( String? reasonDesc; bool banUid = false; late final key = GlobalKey(); - showDialog( + return showDialog( context: context, - builder: (context) => StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - title: const Text('举报'), - 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: Form( - key: key, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: SingleChildScrollView( - child: AnimatedSize( - duration: const Duration(milliseconds: 200), - child: Column( + builder: (context) { + return AlertDialog( + title: const Text('举报'), + 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: Form( + key: key, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: SingleChildScrollView( + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + child: Builder( + builder: (context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( @@ -48,13 +48,20 @@ void autoWrapReportDialog( ), child: Text('请选择举报的理由:'), ), - ...options.entries.map( - (entry) => WrapRadioOptionsGroup( - groupTitle: entry.key, - options: entry.value, - selectedValue: reasonType, - onChanged: (value) => - setState(() => reasonType = value), + RadioGroup( + onChanged: (value) { + reasonType = value; + (context as Element).markNeedsBuild(); + }, + groupValue: reasonType, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: options.entries.map((entry) { + return WrapRadioOptionsGroup( + groupTitle: entry.key, + options: entry.value, + ); + }).toList(), ), ), if (reasonType == 0) @@ -66,51 +73,51 @@ void autoWrapReportDialog( ), ), ), - Padding( - padding: const EdgeInsets.only(left: 14, top: 6), - child: CheckBoxText( - text: '拉黑该用户', - onChanged: (value) => banUid = value, - ), + ), + Padding( + padding: const EdgeInsets.only(left: 14, top: 6), + child: CheckBoxText( + text: '拉黑该用户', + onChanged: (value) => banUid = value, ), - ], + ), + ], + ), + ), + 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: () async { - if (reasonType == null || - (reasonType == 0 && key.currentState?.validate() != true)) { - return; + TextButton( + onPressed: () async { + if (reasonType == null || + (reasonType == 0 && key.currentState?.validate() != true)) { + return; + } + SmartDialog.showLoading(); + try { + final data = await onSuccess(reasonType!, reasonDesc, banUid); + SmartDialog.dismiss(); + if (data['code'] == 0) { + Get.back(); + SmartDialog.showToast('举报成功'); + } else { + SmartDialog.showToast(data['message']); } - SmartDialog.showLoading(); - try { - final data = await onSuccess(reasonType!, reasonDesc, banUid); - SmartDialog.dismiss(); - if (data['code'] == 0) { - Get.back(); - SmartDialog.showToast('举报成功'); - } else { - SmartDialog.showToast(data['message']); - } - } catch (e) { - SmartDialog.dismiss(); - SmartDialog.showToast('提交失败:$e'); - } - }, - child: const Text('确定'), - ), - ], - ); - }, - ), + } catch (e) { + SmartDialog.dismiss(); + SmartDialog.showToast('提交失败:$e'); + } + }, + child: const Text('确定'), + ), + ], + ); + }, ); } @@ -186,8 +193,8 @@ class _CheckBoxTextState extends State { onTap: () { setState(() { _selected = !_selected; + widget.onChanged(_selected); }); - widget.onChanged(_selected); }, child: Padding( padding: const EdgeInsets.all(4), diff --git a/lib/common/widgets/dialog/report_member.dart b/lib/common/widgets/dialog/report_member.dart index 626eae8e..53028e20 100644 --- a/lib/common/widgets/dialog/report_member.dart +++ b/lib/common/widgets/dialog/report_member.dart @@ -1,126 +1,124 @@ -import 'package:PiliPlus/common/widgets/radio_widget.dart'; import 'package:PiliPlus/http/member.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -class MemberReportPanel extends StatefulWidget { - const MemberReportPanel({ - super.key, - required this.name, - required this.mid, - }); +Future showMemberReportDialog( + BuildContext context, { + required Object? name, + required Object mid, +}) { + final List reasonList = List.generate(3, (_) => false); + final Set reason = {}; + int? reasonV2; - final dynamic name; - final dynamic mid; - - @override - State createState() => _MemberReportPanelState(); -} - -class _MemberReportPanelState extends State { - final List _reasonList = List.generate(3, (_) => false); - final Set _reason = {}; - int? _reasonV2; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '举报: ${widget.name}', - style: const TextStyle(fontSize: 18), - ), - const SizedBox(height: 4), - Text('uid: ${widget.mid}'), - const SizedBox(height: 10), - const Text('举报内容(必选,可多选)'), - ...List.generate( - 3, - (index) => _checkBoxWidget( - _reasonList[index], - (value) { - setState(() => _reasonList[index] = value); - if (value) { - _reason.add(index + 1); - } else { - _reason.remove(index + 1); - } - }, - const ['头像违规', '昵称违规', '签名违规'][index], + return showDialog( + context: context, + builder: (context) { + final theme = Theme.of(context); + return AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + titleTextStyle: theme.textTheme.bodyMedium, + title: Column( + spacing: 4, + children: [ + Text( + '举报: $name', + style: const TextStyle(fontSize: 18), ), - ), - const Text('举报理由(单选,非必选)'), - ...List.generate( - 5, - (index) => RadioWidget( - value: index, - groupValue: _reasonV2, - onChanged: (value) { - setState(() => _reasonV2 = value); - }, - title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index], - ), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.end, + Text('uid: $mid'), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle(color: theme.colorScheme.outline), + const Text('举报内容(必选,可多选)'), + ...List.generate( + 3, + (index) => Builder( + builder: (context) => CheckboxListTile( + dense: true, + value: reasonList[index], + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + onChanged: (value) { + reasonList[index] = value!; + if (value) { + reason.add(index + 1); + } else { + reason.remove(index + 1); + } + (context as Element).markNeedsBuild(); + }, + title: Text(const ['头像违规', '昵称违规', '签名违规'][index]), + ), ), ), - TextButton( - onPressed: () async { - if (_reason.isEmpty) { - SmartDialog.showToast('至少选择一项作为举报内容'); - } else { - Get.back(); - var result = await MemberHttp.reportMember( - widget.mid, - reason: _reason.join(','), - reasonV2: _reasonV2 != null ? _reasonV2! + 1 : null, - ); - if (result['msg'] is String && result['msg'].isNotEmpty) { - SmartDialog.showToast(result['msg']); - } else { - SmartDialog.showToast('举报失败'); - } - } - }, - child: const Text('确定'), + const Text('举报理由(单选,非必选)'), + Builder( + builder: (context) => RadioGroup( + onChanged: (v) { + reasonV2 = v; + (context as Element).markNeedsBuild(); + }, + groupValue: reasonV2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + 5, + (index) => RadioListTile( + toggleable: true, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: const EdgeInsets.only(left: 4), + dense: true, + value: index, + title: Text( + const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index], + ), + ), + ), + ), + ), ), ], ), - ], - ), - ); - } -} - -Widget _checkBoxWidget( - bool defValue, - ValueChanged onChanged, - String title, -) { - return InkWell( - onTap: () => onChanged(!defValue), - child: Row( - children: [ - Checkbox( - value: defValue, - onChanged: onChanged, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - Text(title), - ], - ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle(color: theme.colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + if (reason.isEmpty) { + SmartDialog.showToast('至少选择一项作为举报内容'); + } else { + Get.back(); + var result = await MemberHttp.reportMember( + mid, + reason: reason.join(','), + reasonV2: reasonV2 != null ? reasonV2! + 1 : null, + ); + if (result['msg'] is String && result['msg'].isNotEmpty) { + SmartDialog.showToast(result['msg']); + } else { + SmartDialog.showToast('举报失败'); + } + } + }, + child: const Text('确定'), + ), + ], + ); + }, ); } diff --git a/lib/common/widgets/marquee.dart b/lib/common/widgets/marquee.dart index 78515899..6c86f02d 100644 --- a/lib/common/widgets/marquee.dart +++ b/lib/common/widgets/marquee.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; @@ -7,7 +8,7 @@ class MarqueeText extends StatelessWidget { final TextStyle? style; final double spacing; final double velocity; - final MarqueeController? controller; + final ContextSingleTicker? provider; const MarqueeText( this.text, { @@ -15,7 +16,7 @@ class MarqueeText extends StatelessWidget { this.style, this.spacing = 0, this.velocity = 25, - this.controller, + this.provider, }); @override @@ -23,7 +24,7 @@ class MarqueeText extends StatelessWidget { return NormalMarquee( velocity: velocity, spacing: spacing, - controller: controller, + provider: provider, child: Text( text, style: style, @@ -39,7 +40,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget { final Clip clipBehavior; final double spacing; final double velocity; - final MarqueeController? controller; + final ContextSingleTicker? provider; const Marquee({ super.key, @@ -48,7 +49,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget { this.direction = Axis.horizontal, this.clipBehavior = Clip.hardEdge, this.spacing = 0, - this.controller, + this.provider, }); @override @@ -61,6 +62,10 @@ abstract class Marquee extends SingleChildRenderObjectWidget { ..clipBehavior = clipBehavior ..velocity = velocity ..spacing = spacing; + + if (provider != null) { + renderObject.provider = provider!; + } } } @@ -72,7 +77,7 @@ class NormalMarquee extends Marquee { super.direction, super.clipBehavior, super.spacing, - super.controller, + super.provider, }); @override @@ -81,7 +86,7 @@ class NormalMarquee extends Marquee { velocity: velocity, clipBehavior: clipBehavior, spacing: spacing, - controller: controller, + provider: provider ?? ContextSingleTicker(context), ); } @@ -93,6 +98,7 @@ class BounceMarquee extends Marquee { super.direction, super.clipBehavior, super.spacing, + super.provider, }); @override @@ -101,6 +107,7 @@ class BounceMarquee extends Marquee { velocity: velocity, clipBehavior: clipBehavior, spacing: spacing, + provider: provider ?? ContextSingleTicker(context), ); } @@ -111,16 +118,15 @@ abstract class MarqueeRender extends RenderBox required double velocity, required double spacing, required this.clipBehavior, - this.controller, - }) : _spacing = spacing, + required ContextSingleTicker provider, + }) : _ticker = provider, + _spacing = spacing, _velocity = velocity, _direction = direction, assert(spacing.isFinite && !spacing.isNaN); Clip clipBehavior; - MarqueeController? controller; - Axis _direction; Axis get direction => _direction; set direction(Axis value) { @@ -129,12 +135,26 @@ abstract class MarqueeRender extends RenderBox markNeedsLayout(); } + ContextSingleTicker _ticker; + set provider(ContextSingleTicker value) { + if (_ticker == value) return; + if (_ticker._ticker != null) { + if (value._ticker != null) { + value._ticker!.absorbTicker(_ticker._ticker!); + } else { + value.createTicker(_onTick); + } + } + _ticker.cancel(); + _ticker = value; + } + double _velocity; set velocity(double value) { if (_velocity == value) return; _velocity = value; _simulation = _simulation?.copyWith(initialValue: _delta, velocity: value); - controller?.reset(); + _ticker.reset(); } double _spacing; @@ -149,7 +169,7 @@ abstract class MarqueeRender extends RenderBox addSize: value - _spacing, ); _spacing = value; - controller?.reset(); + _ticker.reset(); } double _delta = 0; @@ -160,14 +180,14 @@ abstract class MarqueeRender extends RenderBox } @override - void detach() { - controller?.dispose(); - super.detach(); + void attach(PipelineOwner owner) { + super.attach(owner); + _ticker.updateTicker(); } @override void dispose() { - controller?.dispose(); + _ticker.cancel(); super.dispose(); } @@ -203,11 +223,9 @@ abstract class MarqueeRender extends RenderBox if (_distance > 0) { updateSize(); - (controller ??= MarqueeController()) - ..ticker ??= Ticker(_onTick) - ..initStart(); + _ticker.createTicker(_onTick); } else { - controller?.dispose(); + _ticker.cancel(); } } @@ -237,6 +255,7 @@ class _BounceMarqueeRender extends MarqueeRender { required super.velocity, required super.clipBehavior, required super.spacing, + required super.provider, }); @override @@ -278,7 +297,7 @@ class _NormalMarqueeRender extends MarqueeRender { required super.velocity, required super.clipBehavior, required super.spacing, - super.controller, + required super.provider, }); @override @@ -375,44 +394,56 @@ class _MarqueeSimulation extends Simulation { ); } -extension on Ticker { +class ContextSingleTicker { + Ticker? _ticker; + BuildContext context; + + ContextSingleTicker(this.context); + + void createTicker(TickerCallback onTick) { + assert(() { + if (_ticker == null) { + return true; + } + throw FlutterError.fromParts([ + ErrorSummary( + '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.', + ), + ErrorDescription( + 'A SingleTickerProviderStateMixin can only be used as a TickerProvider once.', + ), + ErrorHint( + 'If a State is used for multiple AnimationController objects, or if it is passed to other ' + 'objects and those objects might use it more than one time in total, then instead of ' + 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.', + ), + ]); + }()); + _ticker = Ticker( + onTick, + debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null, + )..start(); + _tickerModeNotifier = TickerMode.getNotifier(context) + ..addListener(updateTicker); + updateTicker(); // Sets _ticker.mute correctly. + } + void reset() { - this - ..stop() + _ticker + ?..stop() ..start(); } -} - -class MarqueeController { - MarqueeController({this.autoStart = true}); - bool autoStart; - - Ticker? ticker; - - void initStart() { - if (autoStart) { - start(); - } - } - - void start() { - if (ticker != null) { - if (!ticker!.isTicking) { - ticker!.start(); - } - } - } - - void stop() { - ticker?.stop(); - } - - void reset() { - ticker?.reset(); - } - - void dispose() { - ticker?.dispose(); - ticker = null; - } + + void cancel() { + _ticker?.dispose(); + _ticker = null; + _tickerModeNotifier?.removeListener(updateTicker); + _tickerModeNotifier = null; + } + + ValueListenable? _tickerModeNotifier; + + void updateTicker() => _ticker?.muted = !_tickerModeNotifier!.value; + + set muted(bool value) => _ticker?.muted = value; } diff --git a/lib/common/widgets/radio_widget.dart b/lib/common/widgets/radio_widget.dart index 6a978c73..7f9a35c4 100644 --- a/lib/common/widgets/radio_widget.dart +++ b/lib/common/widgets/radio_widget.dart @@ -1,43 +1,88 @@ import 'package:flutter/material.dart'; -class RadioWidget extends StatelessWidget { +class RadioWidget extends StatefulWidget { final T value; - final T? groupValue; - final ValueChanged onChanged; final String title; + final bool tristate; final EdgeInsetsGeometry? padding; + final MainAxisSize mainAxisSize; const RadioWidget({ super.key, required this.value, - this.groupValue, - required this.onChanged, required this.title, + this.tristate = false, this.padding, + this.mainAxisSize = MainAxisSize.min, }); - Widget _child() => Row( - children: [ - Radio( - value: value, - groupValue: groupValue, - onChanged: onChanged, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - Text(title), - ], - ); + @override + State> createState() => RadioWidgetState(); +} + +class RadioWidgetState extends State> with RadioClient { + late final _RadioRegistry _radioRegistry = _RadioRegistry(this); + + @override + final focusNode = FocusNode(); + + @override + T get radioValue => widget.value; + + bool get checked => radioValue == registry!.groupValue; + + @override + bool get tristate => widget.tristate; + + @override + void dispose() { + registry = null; + focusNode.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + registry = RadioGroup.maybeOf(context); + assert(registry != null); + } + + void _handleTap() { + if (checked) { + if (tristate) registry!.onChanged(null); + return; + } + registry!.onChanged(radioValue); + } @override Widget build(BuildContext context) { + final child = Row( + mainAxisSize: widget.mainAxisSize, + children: [ + Focus( + parentNode: focusNode, + canRequestFocus: false, + skipTraversal: true, + includeSemantics: true, + descendantsAreFocusable: false, + descendantsAreTraversable: false, + child: Radio( + value: radioValue, + groupRegistry: _radioRegistry, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + Text(widget.title), + ], + ); return InkWell( - onTap: () => onChanged(value), - child: padding != null - ? Padding( - padding: padding!, - child: _child(), - ) - : _child(), + onTap: _handleTap, + focusNode: focusNode, + child: widget.padding == null + ? child + : Padding(padding: widget.padding!, child: child), ); } } @@ -45,16 +90,12 @@ class RadioWidget extends StatelessWidget { class WrapRadioOptionsGroup extends StatelessWidget { final String groupTitle; final Map options; - final T? selectedValue; - final ValueChanged onChanged; final EdgeInsetsGeometry? itemPadding; const WrapRadioOptionsGroup({ super.key, required this.groupTitle, required this.options, - required this.selectedValue, - required this.onChanged, this.itemPadding, }); @@ -75,14 +116,10 @@ class WrapRadioOptionsGroup extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), child: Wrap( children: options.entries.map((entry) { - return IntrinsicWidth( - child: RadioWidget( - value: entry.key, - groupValue: selectedValue, - onChanged: onChanged, - title: entry.value, - padding: itemPadding ?? const EdgeInsets.only(right: 10), - ), + return RadioWidget( + value: entry.key, + title: entry.value, + padding: itemPadding ?? const EdgeInsets.only(right: 10), ); }).toList(), ), @@ -91,3 +128,27 @@ class WrapRadioOptionsGroup extends StatelessWidget { ); } } + +/// A registry to controls internal [Radio] and hides it from [RadioGroup] +/// ancestor. +/// +/// [RadioListTile] implements the [RadioClient] directly to register to +/// [RadioGroup] ancestor. Therefore, it has to hide the internal [Radio] from +/// participate in the [RadioGroup] ancestor. +class _RadioRegistry extends RadioGroupRegistry { + _RadioRegistry(this.state); + + final RadioWidgetState state; + + @override + T? get groupValue => state.registry!.groupValue; + + @override + ValueChanged get onChanged => state.registry!.onChanged; + + @override + void registerClient(RadioClient radio) {} + + @override + void unregisterClient(RadioClient radio) {} +} diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index 4e184713..0a1451e7 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -212,7 +212,7 @@ class DynamicsHttp { } // - static Future dynamicDetail({ + static Future> dynamicDetail({ dynamic id, dynamic rid, dynamic type, @@ -236,21 +236,12 @@ class DynamicsHttp { ); if (res.data['code'] == 0) { try { - return { - 'status': true, - 'data': DynamicItemModel.fromJson(res.data['data']['item']), - }; + return Success(DynamicItemModel.fromJson(res.data['data']['item'])); } catch (err) { - return { - 'status': false, - 'msg': err.toString(), - }; + return Error(err.toString()); } } else { - return { - 'status': false, - 'msg': res.data['message'], - }; + return Error(res.data['message']); } } diff --git a/lib/http/member.dart b/lib/http/member.dart index c0899723..8fff8885 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -118,7 +118,7 @@ class MemberHttp { int? next, int? seasonId, int? seriesId, - includeCursor, + bool? includeCursor, }) async { final params = { 'aid': ?aid, diff --git a/lib/main.dart b/lib/main.dart index 8786c77d..1f574551 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,38 +33,31 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); await GStorage.init(); - Get.put(AccountService()); - if (Pref.autoClearCache) { - await CacheManage.clearLibraryCache(); - } else { - final num maxCacheSize = Pref.maxCacheSize; - if (maxCacheSize != 0) { - final double currCache = await CacheManage().loadApplicationCache(); - if (currCache >= maxCacheSize) { - await CacheManage.clearLibraryCache(); - } - } - } - if (Pref.horizontalScreen) { - await SystemChrome.setPreferredOrientations( - //支持竖屏与横屏 - [ - DeviceOrientation.portraitUp, - // DeviceOrientation.portraitDown, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ], - ); - } else { - await SystemChrome.setPreferredOrientations( - //支持竖屏 - [ - DeviceOrientation.portraitUp, - ], - ); - } + Get.lazyPut(AccountService.new); HttpOverrides.global = _CustomHttpOverrides(); - await setupServiceLocator(); + + await Future.wait([ + CacheManage.autoClearCache(), + if (Pref.horizontalScreen) + SystemChrome.setPreferredOrientations( + //支持竖屏与横屏 + [ + DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ], + ) + else + SystemChrome.setPreferredOrientations( + //支持竖屏 + [ + DeviceOrientation.portraitUp, + ], + ), + setupServiceLocator(), + ]); + Request(); Request.setCookie(); diff --git a/lib/pages/about/view.dart b/lib/pages/about/view.dart index 6ef67702..c97573e1 100644 --- a/lib/pages/about/view.dart +++ b/lib/pages/about/view.dart @@ -56,9 +56,16 @@ class _AboutPageState extends State { getCurrentApp(); } + @override + void dispose() { + currentVersion.close(); + cacheSize.close(); + super.dispose(); + } + Future getCacheSize() async { cacheSize.value = CacheManage.formatSize( - await CacheManage().loadApplicationCache(), + await CacheManage.loadApplicationCache(), ); } diff --git a/lib/pages/article/controller.dart b/lib/pages/article/controller.dart index 907a6a1a..b1dc4d3c 100644 --- a/lib/pages/article/controller.dart +++ b/lib/pages/article/controller.dart @@ -63,6 +63,7 @@ class ArticleController extends CommonDynController { id = opusId; type = 'opus'; } + Get.putOrFind(() => this, tag: type + id); } init(); }); diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index a1d315b9..a26a11cc 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -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 { @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 { 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 { 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 { Expanded( flex: flex, child: CustomScrollView( - controller: controller.scrollController, + controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( @@ -150,7 +151,7 @@ class _ArticlePageState extends CommonDynPageState { 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 { bottom: 0, right: 0, child: SlideTransition( - position: controller.fabAnim, + position: fabAnim, child: Builder( builder: (context) { if (!controller.showDynActionBar) { diff --git a/lib/pages/common/dyn/common_dyn_controller.dart b/lib/pages/common/dyn/common_dyn_controller.dart index 14edd6ac..d3369046 100644 --- a/lib/pages/common/dyn/common_dyn_controller.dart +++ b/lib/pages/common/dyn/common_dyn_controller.dart @@ -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 - with GetSingleTickerProviderStateMixin { +abstract class CommonDynController extends ReplyController { int get oid; int get replyType; - bool _showFab = true; - late final AnimationController fabAnimationCtr; - late final Animation fabAnim; - late final RxBool showTitle = false.obs; late final horizontalPreview = Pref.horizontalPreview; late final List ratio = Pref.dynamicDetailRatio; - final fabOffset = const Offset(0, 1); - - @override - void onInit() { - fabAnimationCtr = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - ); - fabAnim = - Tween( - 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> customGetData() => ReplyGrpc.mainList( type: replyType, diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart index a2619508..cae4a1df 100644 --- a/lib/pages/common/dyn/common_dyn_page.dart +++ b/lib/pages/common/dyn/common_dyn_page.dart @@ -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 extends State with TickerProviderStateMixin { CommonDynController get controller; + late final scrollController = ScrollController()..addListener(listener); + late final scaffoldKey = GlobalKey(); bool get horizontalPreview => !isPortrait && controller.horizontalPreview; @@ -35,6 +38,49 @@ abstract class CommonDynPageState extends State 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 fabAnim = Tween( + 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 extends State isPortrait = size.isPortrait; imageCallback = horizontalPreview ? (imgList, index) { - controller.hideFab(); + hideFab(); PageUtils.onHorizontalPreview( scaffoldKey, this, @@ -55,6 +101,12 @@ abstract class CommonDynPageState extends State 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 extends State } else { ScaffoldState? scaffoldState = Scaffold.maybeOf(context); if (scaffoldState != null) { - controller.hideFab(); + hideFab(); scaffoldState.showBottomSheet( backgroundColor: Colors.transparent, (context) => replyReplyPage(showBackBtn: false), diff --git a/lib/pages/dynamics_detail/controller.dart b/lib/pages/dynamics_detail/controller.dart index 02c8c8b5..df039e7e 100644 --- a/lib/pages/dynamics_detail/controller.dart +++ b/lib/pages/dynamics_detail/controller.dart @@ -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(); } }); } diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index 442add7e..085e500a 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -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 { @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 { 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 { 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 { Expanded( flex: flex, child: CustomScrollView( - controller: controller.scrollController, + controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( @@ -161,7 +162,7 @@ class _DynamicDetailPageState extends CommonDynPageState { 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 { 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 { 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 { controller.dynItem, () { if (context.mounted) { - (context as Element?)?.markNeedsBuild(); + (context as Element).markNeedsBuild(); } }, ), diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart index 80e14b8f..4b5b77b3 100644 --- a/lib/pages/login/controller.dart +++ b/lib/pages/login/controller.dart @@ -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( + 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( 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('确定'), + ), + ], ), ); } diff --git a/lib/pages/match_info/view.dart b/lib/pages/match_info/view.dart index d5f802c4..28751a46 100644 --- a/lib/pages/match_info/view.dart +++ b/lib/pages/match_info/view.dart @@ -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 { @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 { 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 { ), ), floatingActionButton: SlideTransition( - position: controller.fabAnim, + position: fabAnim, child: replyButton, ), ); diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 7ba8d4db..dfeec6f4 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -55,7 +55,7 @@ class MemberController extends CommonDataController @override bool customHandleResponse(bool isRefresh, Success 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 } Future 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) { diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 2b9e34fc..4b952419 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -58,7 +58,6 @@ class _MemberPageState extends State { 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 { ] 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, diff --git a/lib/pages/member_audio/widgets/item.dart b/lib/pages/member_audio/widgets/item.dart index 27e34ced..20406d89 100644 --- a/lib/pages/member_audio/widgets/item.dart +++ b/lib/pages/member_audio/widgets/item.dart @@ -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), diff --git a/lib/pages/member_contribute/view.dart b/lib/pages/member_contribute/view.dart index 81f5e809..77aca70c 100644 --- a/lib/pages/member_contribute/view.dart +++ b/lib/pages/member_contribute/view.dart @@ -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 @override bool get wantKeepAlive => true; - late final _controller = Get.put( - MemberContributeCtr( + late final _controller = Get.putOrFind( + () => MemberContributeCtr( heroTag: widget.heroTag, initialIndex: widget.initialIndex, ), diff --git a/lib/pages/member_video/controller.dart b/lib/pages/member_video/controller.dart index 8607aa59..ec182833 100644 --- a/lib/pages/member_video/controller.dart +++ b/lib/pages/member_video/controller.dart @@ -40,18 +40,19 @@ class MemberVideoCtr String? firstAid; String? lastAid; String? fromViewAid; - Rx isLocating = Rx(null); - bool? isLoadPrevious; + RxBool isLocating = false.obs; + bool isLoadPrevious = false; bool? hasPrev; @override Future 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 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 ??= []; - 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 onReload() { reload = true; - isLocating.value = null; + isLocating.value = false; return super.onReload(); } } diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 814d105c..1fa377c6 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -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 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 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() diff --git a/lib/pages/music/controller.dart b/lib/pages/music/controller.dart index e7502186..621ae0fa 100644 --- a/lib/pages/music/controller.dart +++ b/lib/pages/music/controller.dart @@ -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(); diff --git a/lib/pages/music/video/controller.dart b/lib/pages/music/video/controller.dart index cb490e70..6709f876 100644 --- a/lib/pages/music/video/controller.dart +++ b/lib/pages/music/video/controller.dart @@ -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?, BgmRecommend> { late final String musicId; @@ -13,9 +15,9 @@ class MusicRecommendController @override void onInit() { super.onInit(); - final Map args = Get.arguments; - musicId = args['id']; - musicDetail = args['detail']; + final MusicRecommendArgs args = Get.arguments; + musicId = args.id; + musicDetail = args.item; queryData(); } diff --git a/lib/pages/music/video/view.dart b/lib/pages/music/video/view.dart index 0d1cdfc2..833d3e7e 100644 --- a/lib/pages/music/video/view.dart +++ b/lib/pages/music/video/view.dart @@ -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 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 child: refreshIndicator( onRefresh: _controller.onRefresh, child: CustomScrollView( - controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ _buildAppBar(theme, padding), diff --git a/lib/pages/music/view.dart b/lib/pages/music/view.dart index a5e0903d..5b5ad3ef 100644 --- a/lib/pages/music/view.dart +++ b/lib/pages/music/view.dart @@ -36,9 +36,9 @@ class MusicDetailPage extends StatefulWidget { class _MusicDetailPageState extends CommonDynPageState { @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 { 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 { Expanded( flex: flex, child: CustomScrollView( - controller: controller.scrollController, + controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( @@ -235,7 +235,7 @@ class _MusicDetailPageState extends CommonDynPageState { 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 { 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 { theme, () => Get.to( const MusicRecommandPage(), - arguments: {'id': controller.musicId, 'detail': item}, + arguments: (id: controller.musicId, item: item), ), ), ], diff --git a/lib/pages/save_panel/view.dart b/lib/pages/save_panel/view.dart index 6cb07500..6d65a923 100644 --- a/lib/pages/save_panel/view.dart +++ b/lib/pages/save_panel/view.dart @@ -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 { //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 { } 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( + 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 { 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 { 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 { Text( DateUtil.format( pubdate, - format: DateUtil.longFormatDs, + format: dateFormat, ), style: TextStyle( color: theme.colorScheme.outline, @@ -577,3 +604,5 @@ class _SavePanelState extends State { ); } } + +enum _CoverType { def16_9, square } diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index 8cac419f..1077a469 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -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 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 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(), ), ), ), diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 72925d98..5a2bcaa6 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -577,7 +577,7 @@ List 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, diff --git a/lib/pages/setting/pages/color_select.dart b/lib/pages/setting/pages/color_select.dart index 6a935321..7da0d964 100644 --- a/lib/pages/setting/pages/color_select.dart +++ b/lib/pages/setting/pages/color_select.dart @@ -98,13 +98,13 @@ class _ColorSelectPageState extends State { ), 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 { 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 { 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 { ), ), 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 { 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 { 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 = Pref.themeType.obs; diff --git a/lib/pages/setting/pages/display_mode.dart b/lib/pages/setting/pages/display_mode.dart index 236a2416..ed2e23f2 100644 --- a/lib/pages/setting/pages/display_mode.dart +++ b/lib/pages/setting/pages/display_mode.dart @@ -75,28 +75,30 @@ class _SetDisplayModeState extends State { ), ), Expanded( - child: ListView.builder( - itemCount: modes.length, - itemBuilder: (context, index) { - final DisplayMode mode = modes[index]; - return RadioListTile( - 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( + value: mode, + title: mode == DisplayMode.auto + ? const Text('自动') + : Text('$mode${mode == active ? ' [系统]' : ''}'), + ); + }, + ), ), ), ], diff --git a/lib/pages/setting/slide_color_picker.dart b/lib/pages/setting/slide_color_picker.dart index 3411b692..6cf1e6b3 100644 --- a/lib/pages/setting/slide_color_picker.dart +++ b/lib/pages/setting/slide_color_picker.dart @@ -40,12 +40,12 @@ class _SlideColorPickerState extends State { 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, diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index ba292fe6..5260a9d0 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -33,24 +33,26 @@ class SelectDialog 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( - 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( + onChanged: Navigator.of(context).pop, + groupValue: value, + child: Column( + mainAxisSize: MainAxisSize.min, + children: List.generate( + values.length, + (index) { + final item = values[index]; + return RadioListTile( + dense: true, + value: item.$1, + title: Text( + item.$2, + style: titleMedium, + ), + subtitle: subtitleBuilder?.call(context, index), + ); + }, + ), ), ), ), diff --git a/lib/pages/sponsor_block/view.dart b/lib/pages/sponsor_block/view.dart index 6bd45585..19f515b4 100644 --- a/lib/pages/sponsor_block/view.dart +++ b/lib/pages/sponsor_block/view.dart @@ -423,7 +423,7 @@ class _SponsorBlockPageState extends State { 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(); diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index cca8e954..623c246a 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -425,7 +425,7 @@ class VideoDetailController extends GetxController bool get showVideoSheet => !horizontalScreen && !isPortrait; int? _lastPos; - List? postList; + List postList = []; RxList segmentList = [].obs; List viewPointList = []; List? segmentProgressList; @@ -1316,9 +1316,8 @@ class VideoDetailController extends GetxController } void onBlock(BuildContext context) { - postList ??= []; - if (postList!.isEmpty) { - postList!.add( + if (postList.isEmpty) { + postList.add( PostSegmentModel( segment: Pair( first: 0, diff --git a/lib/pages/video/post_panel/popup_menu_text.dart b/lib/pages/video/post_panel/popup_menu_text.dart new file mode 100644 index 00000000..bd7c6484 --- /dev/null +++ b/lib/pages/video/post_panel/popup_menu_text.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class PopupMenuText extends StatefulWidget { + final String title; + final T initialValue; + final PopupMenuItemSelected onSelected; + final PopupMenuItemBuilder 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> createState() => _PopupMenuTextState(); +} + +class _PopupMenuTextState extends State> { + 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( + 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, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index 67685b59..f55734a0 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -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 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( + 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 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( + 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 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 { late final VideoDetailController videoDetailController = widget.videoDetailController; late final PlPlayerController plPlayerController = widget.plPlayerController; - late final List? list = videoDetailController.postList; + late final List list = videoDetailController.postList; late final double videoDuration = plPlayerController.durationSeconds.value.inMilliseconds / 1000; @@ -224,7 +216,7 @@ class _PostPanelState extends CommonCollapseSlidePageState { tooltip: '添加片段', onPressed: () { setState(() { - list?.insert( + list.insert( 0, PostSegmentModel( segment: Pair( @@ -263,18 +255,14 @@ class _PostPanelState extends CommonCollapseSlidePageState { 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 { ), ), TextButton( - onPressed: () { - Get.back(); - _onPost(); - }, + onPressed: _onPost, child: const Text('确定提交'), ), ], @@ -310,59 +295,56 @@ class _PostPanelState extends CommonCollapseSlidePageState { ); } - 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 ?? []; - if (videoDetailController.positionSubscription == null) { - videoDetailController.initSkip(); - } - } else { - SmartDialog.showToast('提交失败: ${_errMsg(res)}'); - } - }, + Future _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 ?? []; + if (videoDetailController.positionSubscription == null) { + videoDetailController.initSkip(); + } + } else { + SmartDialog.showToast('提交失败: ${_errMsg(res)}'); + } } String _errMsg(Response res) { @@ -381,216 +363,142 @@ class _PostPanelState extends CommonCollapseSlidePageState { } 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( - initialValue: item.category, - onSelected: (e) { - item.category = e; - List 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( - 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 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( - 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( - 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()), + ); + } + }, + ), + ), + ], ); } } diff --git a/lib/pages/video/send_danmaku/view.dart b/lib/pages/video/send_danmaku/view.dart index d108a638..f21ced03 100644 --- a/lib/pages/video/send_danmaku/view.dart +++ b/lib/pages/video/send_danmaku/view.dart @@ -456,7 +456,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { 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(); diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index f199b88f..ac223653 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -83,9 +83,7 @@ class HeaderControlState extends TripleState { 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 { @override void dispose() { clock?.cancel(); - marqueeController?.dispose(); - marqueeController = null; super.dispose(); } @@ -1934,11 +1930,11 @@ class HeaderControlState extends TripleState { title, spacing: 30, velocity: 30, - controller: _marqueeController, style: const TextStyle( color: Colors.white, fontSize: 16, ), + provider: provider, ); }, ), diff --git a/lib/pages/whisper_link_setting/controller.dart b/lib/pages/whisper_link_setting/controller.dart index 5eb2e4d9..6ba19ad4 100644 --- a/lib/pages/whisper_link_setting/controller.dart +++ b/lib/pages/whisper_link_setting/controller.dart @@ -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, + ); } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index f4cff921..3459aa78 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -23,6 +23,7 @@ import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; +import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; import 'package:PiliPlus/pages/video/post_panel/view.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_control_type.dart'; @@ -187,22 +188,12 @@ class _PLVideoPlayerState extends State super.initState(); _controlsListener = plPlayerController.showControls.listen((bool val) { final visible = val && !plPlayerController.controlsLock.value; + widget.videoDetailController?.headerCtrKey.currentState?.provider.muted = + !visible; if (visible) { animationController.forward(); - widget - .videoDetailController - ?.headerCtrKey - .currentState - ?.marqueeController - ?.start(); } else { animationController.reverse(); - widget - .videoDetailController - ?.headerCtrKey - .currentState - ?.marqueeController - ?.stop(); } }); animationController = AnimationController( @@ -1876,9 +1867,13 @@ class _PLVideoPlayerState extends State ); } + late final segment = Pair( + first: plPlayerController.position.value.inMilliseconds / 1000.0, + second: plPlayerController.position.value.inMilliseconds / 1000.0, + ); Future screenshotWebp() async { final videoCtr = widget.videoDetailController!; - final videoInfo = widget.videoDetailController!.data; + final videoInfo = videoCtr.data; final ids = videoInfo.dash!.video!.map((i) => i.id!).toSet(); final video = videoCtr.findVideoByQa(ids.reduce((p, n) => p < n ? p : n)); @@ -1890,7 +1885,6 @@ class _PLVideoPlayerState extends State final theme = Theme.of(context); final currentPos = ctr.position.value.inMilliseconds / 1000.0; final duration = ctr.durationSeconds.value.inMilliseconds / 1000.0; - final segment = Pair(first: currentPos, second: currentPos + 10.0); final model = PostSegmentModel( segment: segment, category: SegmentType.sponsor, @@ -1915,43 +1909,37 @@ class _PLVideoPlayerState extends State currentPos: currentPos, videoDuration: duration, ), - Builder( - builder: (context) => PopupMenuButton( - initialValue: qa.code, - onSelected: (value) { - if (value == qa.code) return; - final video = videoCtr.findVideoByQa(value); - url = video.baseUrl; - qa = video.quality; - (context as Element).markNeedsBuild(); - }, - itemBuilder: (_) => videoInfo.supportFormats! - .map( - (i) => PopupMenuItem( - enabled: ids.contains(i.quality), - value: i.quality, - child: Text(i.newDesc ?? ''), - ), - ) - .toList(), - child: Text('转码画质:${qa.shortDesc}'), - ), + PopupMenuText( + title: '选择画质', + initialValue: qa.code, + onSelected: (value) { + final video = videoCtr.findVideoByQa(value); + url = video.baseUrl; + qa = video.quality; + }, + itemBuilder: (context) => videoInfo.supportFormats! + .map( + (i) => PopupMenuItem( + enabled: ids.contains(i.quality), + value: i.quality, + child: Text(i.newDesc ?? ''), + ), + ) + .toList(), + getSelectTitle: (_) => qa.shortDesc, ), - Builder( - builder: (context) => PopupMenuButton( - initialValue: preset, - onSelected: (value) { - if (preset == value) return; - preset = value; - (context as Element).markNeedsBuild(); - }, - itemBuilder: (_) => WebpPreset.values - .map( - (i) => PopupMenuItem(value: i, child: Text(i.name)), - ) - .toList(), - child: Text('webp预设:${preset.name}(${preset.desc})'), - ), + PopupMenuText( + title: 'webp预设', + initialValue: preset, + onSelected: (value) { + if (preset == value) return; + preset = value; + (context as Element).markNeedsBuild(); + }, + itemBuilder: (context) => WebpPreset.values + .map((i) => PopupMenuItem(value: i, child: Text(i.name))) + .toList(), + getSelectTitle: (i) => '${i.name}(${i.desc})', ), Text( '*转码使用软解,速度可能慢于播放,请不要选择过长的时间段或过高画质', diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 0455ab29..6179768c 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -242,12 +242,12 @@ class PiliScheme { case 'comment': if (path.startsWith("/detail/")) { // bilibili://comment/detail/17/832703053858603029/238686570016/?subType=0&anchor=238686628816&showEnter=1&extraIntentId=0&scene=1&enterName=%E6%9F%A5%E7%9C%8B%E5%8A%A8%E6%80%81%E8%AF%A6%E6%83%85&enterUri=bilibili://following/detail/832703053858603029 - List pathSegments = uri.pathSegments; - Map queryParameters = uri.queryParameters; - int type = int.parse(pathSegments[1]); // business_id - int oid = int.parse(pathSegments[2]); // subject_id - int rootId = int.parse(pathSegments[3]); // root_id // target_id - int? rpId = + final pathSegments = uri.pathSegments; + final queryParameters = uri.queryParameters; + final type = int.parse(pathSegments[1]); // business_id + final oid = int.parse(pathSegments[2]); // subject_id + final rootId = int.parse(pathSegments[3]); // root_id // target_id + final rpId = queryParameters['anchor'] != null // source_id ? int.tryParse(queryParameters['anchor']!) @@ -255,34 +255,39 @@ class PiliScheme { // int subType = int.parse(queryParameters['subType'] ?? '0'); // int extraIntentId = // int.parse(queryParameters['extraIntentId'] ?? '0'); + final enterUri = queryParameters['enterUri']; Get.to( arguments: { 'oid': oid, 'rpid': rootId, 'id': rpId, 'type': type, - 'enterUri': queryParameters['enterUri'], + 'enterUri': enterUri, }, () => Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( title: const Text('评论详情'), - actions: [ - IconButton( - tooltip: '前往', - onPressed: () { - String? enterUri = queryParameters['enterUri']; - if (enterUri != null) { - routePush(Uri.parse(enterUri)); - } else { - routePush( - Uri.parse('bilibili://following/detail/$oid'), - ); - } - }, - icon: const Icon(Icons.open_in_new), - ), - ], + actions: + enterUri != null || const [11, 16, 17].contains(type) + ? [ + IconButton( + tooltip: '前往', + onPressed: () { + if (enterUri != null) { + routePush(Uri.parse(enterUri)); + } else { + routePush( + Uri.parse( + 'bilibili://following/detail/$oid', + ), + ); + } + }, + icon: const Icon(Icons.open_in_new), + ), + ] + : null, ), body: ViewSafeArea( child: VideoReplyReplyPanel( @@ -729,9 +734,12 @@ class PiliScheme { case 'bangumi': // www.bilibili.com/bangumi/play/ep{eid}?start_progress={offset}&thumb_up_dm_id={dmid} // if (kDebugMode) debugPrint('番剧'); + final queryParameters = uri.queryParameters; bool hasMatch = PageUtils.viewPgcFromUri( path, - progress: uri.queryParameters['start_progress'], + progress: + queryParameters['start_progress'] ?? + queryParameters['dm_progress'], ); if (hasMatch) { return true; diff --git a/lib/utils/cache_manage.dart b/lib/utils/cache_manage.dart index 51b4eb8d..a6cbb137 100644 --- a/lib/utils/cache_manage.dart +++ b/lib/utils/cache_manage.dart @@ -2,17 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:PiliPlus/utils/extension.dart'; +import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:path_provider/path_provider.dart'; -class CacheManage { - CacheManage._internal(); - - static final CacheManage cacheManage = CacheManage._internal(); - - factory CacheManage() => cacheManage; - +abstract class CacheManage { // 获取缓存目录 - Future loadApplicationCache() async { + static Future loadApplicationCache() async { /// clear all of image in memory // clearMemoryImageCache(); /// get ImageCache @@ -47,7 +42,9 @@ class CacheManage { } // 循环计算文件的大小(递归) - Future getTotalSizeOfFilesInDir(final FileSystemEntity file) async { + static Future getTotalSizeOfFilesInDir( + final FileSystemEntity file, + ) async { if (file is File) { int length = await file.length(); return double.parse(length.toString()); @@ -76,7 +73,7 @@ class CacheManage { } /// 清除 Documents 目录下的 DioCache.db - Future clearApplicationCache() async { + static Future clearApplicationCache() async { Directory directory = await getApplicationDocumentsDirectory(); if (directory.existsSync()) { String dioCacheFileName = @@ -103,7 +100,7 @@ class CacheManage { } /// 递归方式删除目录及文件 - Future deleteDirectory(FileSystemEntity file) async { + static Future deleteDirectory(FileSystemEntity file) async { if (file is Directory) { final List children = file.listSync(); for (final FileSystemEntity child in children) { @@ -112,4 +109,18 @@ class CacheManage { } await file.delete(); } + + static Future autoClearCache() async { + if (Pref.autoClearCache) { + await CacheManage.clearLibraryCache(); + } else { + final maxCacheSize = Pref.maxCacheSize; + if (maxCacheSize != 0) { + final currCache = await loadApplicationCache(); + if (currCache >= maxCacheSize) { + await CacheManage.clearLibraryCache(); + } + } + } + } } diff --git a/lib/utils/context_ext.dart b/lib/utils/context_ext.dart index 879566e1..60530a81 100644 --- a/lib/utils/context_ext.dart +++ b/lib/utils/context_ext.dart @@ -52,7 +52,7 @@ extension ContextExtensions on BuildContext { double get devicePixelRatio => MediaQuery.devicePixelRatioOf(this); /// similar to [MediaQuery.of(this).textScaleFactor] - double get textScaleFactor => MediaQuery.textScaleFactorOf(this); + TextScaler get textScaler => MediaQuery.textScalerOf(this); /// get the shortestSide from screen double get mediaQueryShortestSide => mediaQuerySize.shortestSide; diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index 3d9ec25f..6288205d 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -223,3 +223,8 @@ extension FileExt on File { extension SizeExt on Size { bool get isPortrait => width < 600 || height >= width; } + +extension GetExt on GetInterface { + S putOrFind(InstanceBuilderCallback dep, {String? tag}) => + GetInstance().putOrFind(dep, tag: tag); +} diff --git a/lib/utils/page_utils.dart b/lib/utils/page_utils.dart index 520d2289..544ddaaa 100644 --- a/lib/utils/page_utils.dart +++ b/lib/utils/page_utils.dart @@ -292,14 +292,14 @@ class PageUtils { static Future pushDynFromId({id, rid, bool off = false}) async { SmartDialog.showLoading(); - var res = await DynamicsHttp.dynamicDetail( + final res = await DynamicsHttp.dynamicDetail( id: id, rid: rid, type: rid != null ? 2 : null, ); SmartDialog.dismiss(); - if (res['status']) { - DynamicItemModel data = res['data']; + if (res.isSuccess) { + final data = res.data; if (data.basic?.commentType == 12) { toDupNamed( '/articlePage', @@ -313,13 +313,13 @@ class PageUtils { toDupNamed( '/dynamicDetail', arguments: { - 'item': res['data'], + 'item': data, }, off: off, ); } } else { - SmartDialog.showToast(res['msg']); + res.toast(); } } diff --git a/lib/utils/request_utils.dart b/lib/utils/request_utils.dart index 1f92a331..020b5eaf 100644 --- a/lib/utils/request_utils.dart +++ b/lib/utils/request_utils.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:PiliPlus/common/widgets/radio_widget.dart'; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -288,17 +287,17 @@ class RequestUtils { if (id != null) { await Future.delayed(const Duration(milliseconds: 200)); var res = await DynamicsHttp.dynamicDetail(id: id); - if (res['status']) { + if (res.isSuccess) { final ctr = Get.find(tag: 'all'); if (ctr.loadingState.value.isSuccess) { List? list = ctr.loadingState.value.data; if (list != null) { - list.insert(0, res['data']); + list.insert(0, res.data); ctr.loadingState.refresh(); return; } } - ctr.loadingState.value = Success([res['data']]); + ctr.loadingState.value = Success([res.data]); } } } catch (e) { @@ -318,7 +317,7 @@ class RequestUtils { await Future.delayed(const Duration(seconds: 5)); } var res = await DynamicsHttp.dynamicDetail(id: id, clearCookie: true); - bool isBan = !res['status']; + bool isBan = !res.isSuccess; Get.dialog( AlertDialog( title: const Text('动态检查结果'), @@ -400,23 +399,20 @@ class RequestUtils { title: Text('${isCopy ? '复制' : '移动'}到'), contentPadding: const EdgeInsets.only(top: 5), 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: checkedId, + child: RadioGroup( + onChanged: (value) { + checkedId = value; + (context as Element).markNeedsBuild(); + }, + groupValue: checkedId, + child: Column( + children: list.map((item) { + return RadioListTile( + dense: true, + title: Text(item.title), value: item.id, - onChanged: (value) { - checkedId = value; - if (context.mounted) { - (context as Element).markNeedsBuild(); - } - }, ); - }), + }).toList(), ), ), ), diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 3744c844..9b29f10f 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -12,7 +12,7 @@ import 'package:PiliPlus/utils/set_int_adapter.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; -class GStorage { +abstract class GStorage { static late final Box userInfo; static late final Box historyWord; static late final Box localCache; diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 120b0392..dc280c8e 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -33,7 +33,7 @@ import 'package:get/get.dart' hide ContextExtensionss; import 'package:hive/hive.dart'; import 'package:uuid/uuid.dart'; -class Pref { +abstract class Pref { static final Box _setting = GStorage.setting; static final Box _video = GStorage.video; static final Box _localCache = GStorage.localCache;