diff --git a/lib/common/widgets/flutter/list_tile.dart b/lib/common/widgets/flutter/list_tile.dart index e23ee084d..c655257b9 100644 --- a/lib/common/widgets/flutter/list_tile.dart +++ b/lib/common/widgets/flutter/list_tile.dart @@ -335,6 +335,7 @@ class ListTile extends StatelessWidget { this.contentPadding, this.enabled = true, this.onTap, + this.onTapUp, this.onLongPress, this.onSecondaryTap, this.onSecondaryTapUp, @@ -563,6 +564,8 @@ class ListTile extends StatelessWidget { /// Inoperative if [enabled] is false. final GestureTapCallback? onTap; + final GestureTapUpCallback? onTapUp; + /// Called when the user long-presses on this list tile. /// /// Inoperative if [enabled] is false. @@ -984,6 +987,7 @@ class ListTile extends StatelessWidget { return InkWell( customBorder: shape ?? tileTheme.shape, onTap: enabled ? onTap : null, + onTapUp: enabled ? onTapUp : null, onLongPress: enabled ? onLongPress : null, onSecondaryTap: enabled ? onSecondaryTap : null, onSecondaryTapUp: enabled ? onSecondaryTapUp : null, diff --git a/lib/models/common/super_resolution_type.dart b/lib/models/common/super_resolution_type.dart index e7e1bee27..0ca532fc0 100644 --- a/lib/models/common/super_resolution_type.dart +++ b/lib/models/common/super_resolution_type.dart @@ -1,9 +1,12 @@ -enum SuperResolutionType { +import 'package:PiliPlus/models/common/enum_with_label.dart'; + +enum SuperResolutionType with EnumWithLabel { disable('禁用'), efficiency('效率'), quality('画质') ; - final String title; - const SuperResolutionType(this.title); + @override + final String label; + const SuperResolutionType(this.label); } diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index ef3461cce..4e1b28055 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -84,12 +84,15 @@ List get extraSettings => [ ], ), ), - getPopupMenuModel( + PopupModel( title: '番剧片头/片尾跳过类型', leading: const Icon(MdiIcons.debugStepOver), - key: SettingBoxKey.pgcSkipType, - values: SkipType.values, - defaultIndex: SkipType.skipOnce.index, + value: () => Pref.pgcSkipType, + items: SkipType.values, + onSelected: (value, setState) async { + await GStorage.setting.put(SettingBoxKey.pgcSkipType, value.index); + setState(); + }, ), SwitchModel( title: '检查未读动态', @@ -295,7 +298,7 @@ List get extraSettings => [ title: '超分辨率', leading: const Icon(Icons.stay_current_landscape_outlined), getSubtitle: () => - '当前:「${Pref.superResolutionType.title}」\n默认设置对番剧生效, 其他视频默认关闭\n超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换硬件解码器为 auto-copy', + '当前:「${Pref.superResolutionType.label}」\n默认设置对番剧生效, 其他视频默认关闭\n超分辨率需要启用硬件解码, 若启用硬件解码后仍然不生效, 尝试切换硬件解码器为 auto-copy', onTap: _showSuperResolutionDialog, ), const SwitchModel( @@ -964,7 +967,6 @@ Future _showRefreshDragDialog( kDragContainerExtentPercentage = res; await GStorage.setting.put(SettingBoxKey.refreshDragPercentage, res); Get.forceAppUpdate(); - setState(); } } @@ -986,7 +988,6 @@ Future _showRefreshDialog( displacement = res; await GStorage.setting.put(SettingBoxKey.refreshDisplacement, res); Get.forceAppUpdate(); - setState(); } } @@ -999,7 +1000,7 @@ Future _showSuperResolutionDialog( builder: (context) => SelectDialog( title: '超分辨率', value: Pref.superResolutionType, - values: SuperResolutionType.values.map((e) => (e, e.title)).toList(), + values: SuperResolutionType.values.map((e) => (e, e.label)).toList(), ), ); if (res != null) { diff --git a/lib/pages/setting/models/model.dart b/lib/pages/setting/models/model.dart index 540406935..90e3805fc 100644 --- a/lib/pages/setting/models/model.dart +++ b/lib/pages/setting/models/model.dart @@ -1,14 +1,14 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/models/common/enum_with_label.dart'; import 'package:PiliPlus/pages/setting/widgets/normal_item.dart'; +import 'package:PiliPlus/pages/setting/widgets/popup_item.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/pages/setting/widgets/switch_item.dart'; import 'package:PiliPlus/utils/storage.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide PopupMenuItemSelected; import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @immutable sealed class SettingsModel { @@ -30,6 +30,45 @@ sealed class SettingsModel { }); } +class PopupModel extends SettingsModel { + const PopupModel({ + required this.title, + super.subtitle, + super.leading, + super.contentPadding, + super.titleStyle, + required this.value, + required this.items, + required this.onSelected, + }); + + @override + String? get effectiveSubtitle => null; + + @override + String get effectiveTitle => title; + + @override + final String title; + + final ValueGetter value; + final List items; + final PopupMenuItemSelected onSelected; + + @override + Widget get widget => PopupListTile( + safeArea: false, + leading: leading, + title: Text(title), + value: () { + final v = value(); + return (v, v.label); + }, + itemBuilder: (_) => enumItemBuilder(items), + onSelected: onSelected, + ); +} + class NormalModel extends SettingsModel { @override final String? title; @@ -254,61 +293,3 @@ SettingsModel getVideoFilterSelectModel({ }, ); } - -SettingsModel getPopupMenuModel({ - required String title, - Widget? leading, - String? subtitle, - required String key, - required List values, - int defaultIndex = 0, -}) { - // final globalKey = GlobalKey>(); - return NormalModel( - title: title, - subtitle: subtitle, - leading: leading, - // onTap: (context, setState) => globalKey.currentState?.showButtonMenu(), - getTrailing: (theme) => Builder( - builder: (context) { - final color = theme.colorScheme.secondary; - final v = values[GStorage.setting.get(key, defaultValue: defaultIndex)]; - return PopupMenuButton( - // key: globalKey, - padding: .zero, - initialValue: v, - onSelected: (value) async { - await GStorage.setting.put(key, value.index); - if (context.mounted) { - (context as Element).markNeedsBuild(); - } - }, - itemBuilder: (context) => values - .map((i) => PopupMenuItem(value: i, child: Text(i.label))) - .toList(), - child: Padding( - padding: const .symmetric(vertical: 8), - child: Text.rich( - style: TextStyle(fontSize: 14, height: 1, color: color), - strutStyle: const StrutStyle(leading: 0, height: 1, fontSize: 14), - TextSpan( - children: [ - TextSpan(text: v.label), - WidgetSpan( - alignment: .middle, - child: Icon( - size: 14, - MdiIcons.unfoldMoreHorizontal, - color: color, - ), - ), - ], - style: TextStyle(color: color), - ), - ), - ), - ); - }, - ), - ); -} diff --git a/lib/pages/setting/pages/color_select.dart b/lib/pages/setting/pages/color_select.dart index 4279994e0..bf6c7cb2a 100644 --- a/lib/pages/setting/pages/color_select.dart +++ b/lib/pages/setting/pages/color_select.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/models/common/theme/theme_color_type.dart'; import 'package:PiliPlus/models/common/theme/theme_type.dart'; import 'package:PiliPlus/pages/home/view.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; +import 'package:PiliPlus/pages/setting/widgets/popup_item.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/storage.dart'; @@ -76,11 +77,7 @@ class _ColorSelectPageState extends State { Get.changeThemeMode(result.toThemeMode); } }, - leading: Container( - width: 40, - alignment: Alignment.center, - child: const Icon(Icons.flashlight_on_outlined), - ), + leading: const Icon(Icons.flashlight_on_outlined), title: Text('主题模式', style: titleStyle), subtitle: Obx( () => Text( @@ -90,94 +87,57 @@ class _ColorSelectPageState extends State { ), ), Obx( - () => ListTile( + () => PopupListTile( enabled: !ctr.dynamicColor.value, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('调色板风格'), - PopupMenuButton( - enabled: !ctr.dynamicColor.value, - initialValue: _dynamicSchemeVariant, - onSelected: (item) { - _dynamicSchemeVariant = item; - GStorage.setting.put( - SettingBoxKey.schemeVariant, - item.index, - ); - Get.forceAppUpdate(); - }, - itemBuilder: (context) => FlexSchemeVariant.values - .map( - (item) => PopupMenuItem( - value: item, - child: Text(item.variantName), - ), - ) - .toList(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _dynamicSchemeVariant.variantName, - style: TextStyle( - height: 1, - fontSize: 13, - color: ctr.dynamicColor.value - ? theme.colorScheme.outline.withValues( - alpha: 0.8, - ) - : theme.colorScheme.secondary, - ), - strutStyle: const StrutStyle(leading: 0, height: 1), - ), - Icon( - size: 20, - Icons.keyboard_arrow_right, - color: ctr.dynamicColor.value - ? theme.colorScheme.outline.withValues( - alpha: 0.8, - ) - : theme.colorScheme.secondary, - ), - ], - ), - ), - ], - ), - leading: Container( - width: 40, - alignment: Alignment.center, - child: const Icon(Icons.palette_outlined), - ), - subtitle: Text( - _dynamicSchemeVariant.description, - style: const TextStyle(fontSize: 12), - ), + leading: const Icon(Icons.palette_outlined), + title: const Text('调色板风格'), + value: () => + (_dynamicSchemeVariant, _dynamicSchemeVariant.variantName), + itemBuilder: (_) => FlexSchemeVariant.values + .map( + (e) => PopupMenuItem(value: e, child: Text(e.variantName)), + ) + .toList(), + onSelected: (value, setState) { + _dynamicSchemeVariant = value; + GStorage.setting.put(SettingBoxKey.schemeVariant, value.index); + Get.forceAppUpdate(); + }, ), ), if (!Platform.isIOS) Obx( - () => CheckboxListTile( - title: const Text('动态取色'), - controlAffinity: ListTileControlAffinity.leading, - value: ctr.dynamicColor.value, - onChanged: (val) async { - ctr - ..dynamicColor.value = val! - ..setting.put(SettingBoxKey.dynamicColor, val); - if (val) { - if (await MyApp.initPlatformState()) { - Get.forceAppUpdate(); + () { + final dynamicColor = ctr.dynamicColor.value; + return ListTile( + title: const Text('动态取色'), + leading: Checkbox( + value: dynamicColor, + onChanged: (value) {}, + materialTapTargetSize: .shrinkWrap, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + onTap: () async { + final val = !dynamicColor; + if (val) { + if (await MyApp.initPlatformState()) { + Get.forceAppUpdate(); + } else { + SmartDialog.showToast('该设备可能不支持动态取色'); + return; + } } else { - SmartDialog.showToast('该设备可能不支持动态取色'); - ctr.dynamicColor.value = false; + Get.forceAppUpdate(); } - } else { - Get.forceAppUpdate(); - } - }, - ), + ctr + ..dynamicColor.value = val + ..setting.put(SettingBoxKey.dynamicColor, val); + }, + ); + }, ), Padding( padding: padding, diff --git a/lib/pages/setting/widgets/popup_item.dart b/lib/pages/setting/widgets/popup_item.dart new file mode 100644 index 000000000..cd1da8af4 --- /dev/null +++ b/lib/pages/setting/widgets/popup_item.dart @@ -0,0 +1,120 @@ +import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/models/common/enum_with_label.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:flutter/material.dart' hide ListTile; + +typedef PopupMenuItemSelected = + void Function(T value, VoidCallback setState); + +List> enumItemBuilder( + List items, +) => items.map((e) => PopupMenuItem(value: e, child: Text(e.label))).toList(); + +enum DescPosType { subtitle, title, trailing } + +class PopupListTile extends StatefulWidget { + const PopupListTile({ + super.key, + this.dense, + this.safeArea = true, + this.enabled = true, + this.leading, + required this.title, + this.descPosType = .subtitle, + required this.value, + required this.itemBuilder, + required this.onSelected, + }); + + final bool? dense; + final bool safeArea; + final bool enabled; + final Widget? leading; + final Widget title; + + final DescPosType descPosType; + final ValueGetter<(T, String)> value; + final PopupMenuItemBuilder itemBuilder; + final PopupMenuItemSelected onSelected; + + @override + State> createState() => _PopupListTileState(); +} + +class _PopupListTileState extends State> { + final _key = GlobalKey(); + + void _showButtonMenu(TapUpDetails details, T value) { + final box = context.findRenderObject() as RenderBox; + final offset = box.localToGlobal(box.size.topLeft(.zero)); + final double dx; + if (PlatformUtils.isDesktop) { + dx = details.globalPosition.dx + 1; + } else { + final box = _key.currentContext!.findRenderObject() as RenderBox; + final offset = box.localToGlobal(box.size.topLeft(.zero)); + dx = offset.dx; + } + showMenu( + context: context, + position: RelativeRect.fromLTRB(dx, offset.dy + 5, dx, 0), + items: widget.itemBuilder(context), + initialValue: value, + requestFocus: false, + ).then((T? newValue) { + if (!mounted) { + return; + } + if (newValue == null || newValue == value) { + return; + } + widget.onSelected(newValue, _refresh); + }); + } + + void _refresh() { + if (mounted) { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final (value, descStr) = widget.value(); + Widget title = Builder(key: _key, builder: (_) => widget.title); + Widget? subtitle; + Widget? trailing; + final desc = Text( + descStr, + style: TextStyle( + fontSize: 13, + color: widget.enabled + ? theme.colorScheme.secondary + : theme.disabledColor, + ), + ); + switch (widget.descPosType) { + case DescPosType.subtitle: + subtitle = desc; + case DescPosType.title: + title = Row( + spacing: 12, + mainAxisSize: .min, + children: [title, desc], + ); + case DescPosType.trailing: + trailing = desc; + } + return ListTile( + dense: widget.dense, + safeArea: widget.safeArea, + enabled: widget.enabled, + onTapUp: (details) => _showButtonMenu(details, value), + leading: widget.leading, + title: title, + subtitle: subtitle, + trailing: trailing, + ); + } +} diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index f5e3c5d4c..41a0fd53d 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -23,6 +23,7 @@ import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_play_info/subtitle.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; +import 'package:PiliPlus/pages/setting/widgets/popup_item.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/local/controller.dart'; @@ -448,79 +449,25 @@ class HeaderControlState extends State title: const Text('重载视频', style: titleStyle), ), ], - ListTile( + PopupListTile( dense: true, leading: const Icon( Icons.stay_current_landscape_outlined, size: 20, ), - title: Row( - children: [ - const Text( - '超分辨率', - strutStyle: StrutStyle(leading: 0, height: 1), - style: TextStyle( - height: 1, - fontSize: 14, - ), - ), - const SizedBox(width: 10), - Builder( - builder: (context) => PopupMenuButton( - initialValue: - plPlayerController.superResolutionType.value, - child: Padding( - padding: const EdgeInsets.all(4), - child: Text.rich( - style: TextStyle( - height: 1, - fontSize: 14, - color: theme.colorScheme.secondary, - ), - strutStyle: const StrutStyle( - leading: 0, - height: 1, - fontSize: 14, - ), - TextSpan( - children: [ - TextSpan( - text: widget - .controller - .superResolutionType - .value - .title, - ), - WidgetSpan( - alignment: .middle, - child: Icon( - MdiIcons.unfoldMoreHorizontal, - size: 14, - color: theme.colorScheme.secondary, - ), - ), - ], - ), - ), - ), - onSelected: (value) { - plPlayerController.setShader(value); - if (context.mounted) { - (context as Element).markNeedsBuild(); - } - }, - itemBuilder: (context) => SuperResolutionType.values - .map( - (item) => PopupMenuItem( - value: item, - child: Text(item.title), - ), - ) - .toList(), - ), - ), - ], + title: const Text('超分辨率'), + value: () { + final value = plPlayerController.superResolutionType.value; + return (value, value.label); + }, + itemBuilder: (_) => enumItemBuilder( + SuperResolutionType.values, ), + onSelected: (value, setState) { + plPlayerController.setShader(value); + setState(); + }, + descPosType: .title, ), if (!isFileSource) ListTile( diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index dbae30463..940fee7d4 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -455,7 +455,7 @@ class _PLVideoPlayerState extends State value: type, onTap: () => plPlayerController.setShader(type), child: Text( - type.title, + type.label, style: const TextStyle( color: Colors.white, fontSize: 13, @@ -468,7 +468,7 @@ class _PLVideoPlayerState extends State child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( - type.title, + type.label, style: const TextStyle(color: Colors.white, fontSize: 13), ), ),