mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: OrderedMultiSelectDialog (#1290)
* tweak * feat: OrderedMultiSelectDialog
This commit is contained in:
committed by
GitHub
parent
96586f130f
commit
96539cc64c
@@ -25,7 +25,7 @@ class MemberAudioItem extends StatelessWidget {
|
||||
onTap: () async {
|
||||
// TODO music play
|
||||
final aid = item.aid;
|
||||
if (aid != null) {
|
||||
if (aid != null && aid != 0) {
|
||||
final cid = await SearchHttp.ab2c(aid: aid);
|
||||
if (cid != null) {
|
||||
PageUtils.toVideoPage(cid: cid, aid: aid);
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:PiliPlus/models/common/video/live_quality.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_decode_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_quality.dart';
|
||||
import 'package:PiliPlus/pages/setting/models/model.dart';
|
||||
import 'package:PiliPlus/pages/setting/widgets/multi_select_dialog.dart';
|
||||
import 'package:PiliPlus/pages/setting/widgets/ordered_multi_select_dialog.dart';
|
||||
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/hwdec_type.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
@@ -345,10 +345,10 @@ List<SettingsModel> get videoSettings => [
|
||||
leading: const Icon(Icons.memory_outlined),
|
||||
getSubtitle: () => '当前:${Pref.hardwareDecoding}(此项即mpv的--hwdec)',
|
||||
onTap: (setState) async {
|
||||
final result = await showDialog<Set<String>>(
|
||||
final result = await showDialog<List<String>>(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return MultiSelectDialog<String>(
|
||||
return OrderedMultiSelectDialog<String>(
|
||||
title: '硬解模式',
|
||||
initValues: Pref.hardwareDecoding.split(','),
|
||||
values: {
|
||||
|
||||
58
lib/pages/setting/widgets/checkbox_num.dart
Normal file
58
lib/pages/setting/widgets/checkbox_num.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OrderedCheckbox extends StatelessWidget {
|
||||
const OrderedCheckbox({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
}) : assert(value == null || value < 100);
|
||||
|
||||
final int? value;
|
||||
final ValueChanged<int?>? onChanged;
|
||||
bool get selected => value != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final child = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(1.5)),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface,
|
||||
width: 1.6,
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
),
|
||||
color: selected ? theme.colorScheme.primary : null,
|
||||
),
|
||||
child: selected
|
||||
? SizedBox.square(
|
||||
dimension: 16.5,
|
||||
child: Center(
|
||||
child: Text(
|
||||
value.toString(),
|
||||
style: TextStyle(
|
||||
inherit: false,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: theme.iconTheme.shadows,
|
||||
height: 1.0,
|
||||
leadingDistribution: TextLeadingDistribution.even,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.square(dimension: 16.5),
|
||||
);
|
||||
if (onChanged != null) {
|
||||
return InkWell(
|
||||
onTap: () => onChanged!(value),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
214
lib/pages/setting/widgets/checkbox_num_list_tile.dart
Normal file
214
lib/pages/setting/widgets/checkbox_num_list_tile.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'package:PiliPlus/pages/setting/widgets/checkbox_num.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OrderedCheckboxListTile extends StatelessWidget {
|
||||
/// Creates a combination of a list tile and a checkbox.
|
||||
///
|
||||
/// The checkbox tile itself does not maintain any state. Instead, when the
|
||||
/// state of the checkbox changes, the widget calls the [onChanged] callback.
|
||||
/// Most widgets that use a checkbox will listen for the [onChanged] callback
|
||||
/// and rebuild the checkbox tile with a new [value] to update the visual
|
||||
/// appearance of the checkbox.
|
||||
///
|
||||
/// The following arguments are required:
|
||||
///
|
||||
/// * [value], which determines whether the checkbox is checked. The [value]
|
||||
/// can only be null if [tristate] is true.
|
||||
/// * [onChanged], which is called when the value of the checkbox should
|
||||
/// change. It can be set to null to disable the checkbox.
|
||||
const OrderedCheckboxListTile({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.activeColor,
|
||||
this.visualDensity,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.shape,
|
||||
this.tileColor,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.isThreeLine,
|
||||
this.dense,
|
||||
this.trailing,
|
||||
this.contentPadding,
|
||||
this.selectedTileColor,
|
||||
this.onFocusChange,
|
||||
this.enableFeedback,
|
||||
this.checkboxScaleFactor = 1.0,
|
||||
this.titleAlignment,
|
||||
this.internalAddSemanticForOnTap = false,
|
||||
}) : assert(isThreeLine != true || subtitle != null);
|
||||
|
||||
/// Whether this checkbox is checked.
|
||||
final int? value;
|
||||
|
||||
/// Called when the value of the checkbox should change.
|
||||
///
|
||||
/// The checkbox passes the new value to the callback but does not actually
|
||||
/// change state until the parent widget rebuilds the checkbox tile with the
|
||||
/// new value.
|
||||
///
|
||||
/// If null, the checkbox will be displayed as disabled.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// The callback provided to [onChanged] should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt; for example:
|
||||
///
|
||||
/// ```dart
|
||||
/// CheckboxListTile(
|
||||
/// value: _throwShotAway,
|
||||
/// onChanged: (bool? newValue) {
|
||||
/// setState(() {
|
||||
/// _throwShotAway = newValue;
|
||||
/// });
|
||||
/// },
|
||||
/// title: const Text('Throw away your shot'),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
final ValueChanged<int?>? onChanged;
|
||||
|
||||
/// The color to use when this checkbox is checked.
|
||||
///
|
||||
/// Defaults to [ColorScheme.secondary] of the current [Theme].
|
||||
final Color? activeColor;
|
||||
|
||||
/// Defines how compact the list tile's layout will be.
|
||||
///
|
||||
/// {@macro flutter.material.themedata.visualDensity}
|
||||
final VisualDensity? visualDensity;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.material.ListTile.shape}
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// {@macro flutter.material.ListTile.tileColor}
|
||||
final Color? tileColor;
|
||||
|
||||
/// The primary content of the list tile.
|
||||
///
|
||||
/// Typically a [Text] widget.
|
||||
final Widget? title;
|
||||
|
||||
/// Additional content displayed below the title.
|
||||
///
|
||||
/// Typically a [Text] widget.
|
||||
final Widget? subtitle;
|
||||
|
||||
/// A widget to display on the opposite side of the tile from the checkbox.
|
||||
///
|
||||
/// Typically an [Icon] widget.
|
||||
final Widget? trailing;
|
||||
|
||||
/// Whether this list tile is intended to display three lines of text.
|
||||
///
|
||||
/// If null, the value from [ListTileThemeData.isThreeLine] is used.
|
||||
/// If that is also null, the value from [ThemeData.listTileTheme] is used.
|
||||
/// If still null, the default value is `false`.
|
||||
final bool? isThreeLine;
|
||||
|
||||
/// Whether this list tile is part of a vertically dense list.
|
||||
///
|
||||
/// If this property is null then its value is based on [ListTileThemeData.dense].
|
||||
final bool? dense;
|
||||
|
||||
/// Defines insets surrounding the tile's contents.
|
||||
///
|
||||
/// This value will surround the [Checkbox], [title], [subtitle], and [trailing]
|
||||
/// widgets in [OrderedCheckboxListTile].
|
||||
///
|
||||
/// When the value is null, the [contentPadding] is `EdgeInsets.symmetric(horizontal: 16.0)`.
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
/// If non-null, defines the background color when [OrderedCheckboxListTile.selected] is true.
|
||||
final Color? selectedTileColor;
|
||||
|
||||
/// {@macro flutter.material.inkwell.onFocusChange}
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// {@macro flutter.material.ListTile.enableFeedback}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Feedback] for providing platform-specific feedback to certain actions.
|
||||
final bool? enableFeedback;
|
||||
|
||||
/// Defines how [ListTile.leading] and [ListTile.trailing] are
|
||||
/// vertically aligned relative to the [ListTile]'s titles
|
||||
/// ([ListTile.title] and [ListTile.subtitle]).
|
||||
///
|
||||
/// If this property is null then [ListTileThemeData.titleAlignment]
|
||||
/// is used. If that is also null then [ListTileTitleAlignment.threeLine]
|
||||
/// is used.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
|
||||
/// [ListTileThemeData].
|
||||
final ListTileTitleAlignment? titleAlignment;
|
||||
|
||||
/// Whether to add button:true to the semantics if onTap is provided.
|
||||
/// This is a temporary flag to help changing the behavior of ListTile onTap semantics.
|
||||
///
|
||||
// TODO(hangyujin): Remove this flag after fixing related g3 tests and flipping
|
||||
// the default value to true.
|
||||
final bool internalAddSemanticForOnTap;
|
||||
|
||||
/// Controls the scaling factor applied to the [Checkbox] within the [OrderedCheckboxListTile].
|
||||
///
|
||||
/// Defaults to 1.0.
|
||||
final double checkboxScaleFactor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget control;
|
||||
|
||||
control = OrderedCheckbox(value: value, onChanged: null);
|
||||
if (checkboxScaleFactor != 1.0) {
|
||||
control = Transform.scale(scale: checkboxScaleFactor, child: control);
|
||||
}
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context);
|
||||
final Set<WidgetState> states = <WidgetState>{
|
||||
if (value != null) WidgetState.selected,
|
||||
};
|
||||
final Color effectiveActiveColor =
|
||||
activeColor ??
|
||||
checkboxTheme.fillColor?.resolve(states) ??
|
||||
theme.colorScheme.secondary;
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
selectedColor: effectiveActiveColor,
|
||||
leading: control,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
isThreeLine: isThreeLine,
|
||||
dense: dense,
|
||||
enabled: onChanged != null,
|
||||
onTap: onChanged != null ? () => onChanged!(value) : null,
|
||||
selected: value != null,
|
||||
autofocus: autofocus,
|
||||
contentPadding: contentPadding,
|
||||
shape: shape,
|
||||
selectedTileColor: selectedTileColor,
|
||||
tileColor: tileColor,
|
||||
visualDensity: visualDensity,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
enableFeedback: enableFeedback,
|
||||
titleAlignment: titleAlignment,
|
||||
internalAddSemanticForOnTap: internalAddSemanticForOnTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,12 @@ class _MultiSelectDialogState<T> extends State<MultiSelectDialog<T>> {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
title: Text(widget.title),
|
||||
contentPadding: const EdgeInsets.only(top: 12),
|
||||
content: StatefulBuilder(
|
||||
builder: (context, StateSetter setState) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.values.entries.map((i) {
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.values.entries.map((i) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
bool isChecked = _tempValues.contains(i.key);
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
@@ -52,13 +52,13 @@ class _MultiSelectDialogState<T> extends State<MultiSelectDialog<T>> {
|
||||
isChecked
|
||||
? _tempValues.remove(i.key)
|
||||
: _tempValues.add(i.key);
|
||||
setState(() {});
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
actions: [
|
||||
|
||||
96
lib/pages/setting/widgets/ordered_multi_select_dialog.dart
Normal file
96
lib/pages/setting/widgets/ordered_multi_select_dialog.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:PiliPlus/pages/setting/widgets/checkbox_num_list_tile.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class OrderedMultiSelectDialog<T> extends StatefulWidget {
|
||||
final Iterable<T> initValues;
|
||||
final String title;
|
||||
final Map<T, String> values;
|
||||
|
||||
const OrderedMultiSelectDialog({
|
||||
super.key,
|
||||
required this.initValues,
|
||||
required this.values,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderedMultiSelectDialog<T>> createState() =>
|
||||
_OrderedMultiSelectDialogState<T>();
|
||||
}
|
||||
|
||||
class _OrderedMultiSelectDialogState<T>
|
||||
extends State<OrderedMultiSelectDialog<T>> {
|
||||
late Map<T, int> _tempValues;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempValues = {for (var (i, j) in widget.initValues.indexed) j: i + 1};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
title: Text(widget.title),
|
||||
contentPadding: const EdgeInsets.only(top: 12),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.values.entries.map((i) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return OrderedCheckboxListTile(
|
||||
dense: true,
|
||||
value: _tempValues[i.key],
|
||||
title: Text(
|
||||
i.value,
|
||||
style: theme.textTheme.titleMedium!,
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
_tempValues[i.key] = _tempValues.length + 1;
|
||||
(context as Element).markNeedsBuild();
|
||||
} else {
|
||||
final pos = _tempValues.remove(i.key)!;
|
||||
if (pos == _tempValues.length + 1) {
|
||||
(context as Element).markNeedsBuild();
|
||||
} else {
|
||||
_tempValues.updateAll(
|
||||
(key, value) => value > pos ? value - 1 : value,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
assert(_tempValues.values.isSorted((a, b) => a.compareTo(b)));
|
||||
Get.back(result: _tempValues.keys.toList());
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,10 @@ class _SettingsSearchPageState
|
||||
(item.title ?? item.getTitle!()).toLowerCase().contains(
|
||||
value,
|
||||
) ||
|
||||
item.subtitle?.toLowerCase().contains(value) == true,
|
||||
(item.subtitle ?? item.getSubtitle?.call())
|
||||
?.toLowerCase()
|
||||
.contains(value) ==
|
||||
true,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -648,71 +648,77 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: GestureDetector(
|
||||
onTap: () => SmartDialog.showToast(
|
||||
'标灰画质需要bilibili会员(已是会员?请关闭无痕模式);4k和杜比视界播放效果可能不佳',
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('选择画质', style: titleStyle),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
],
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
child: GestureDetector(
|
||||
onTap: () => SmartDialog.showToast(
|
||||
'标灰画质需要bilibili会员(已是会员?请关闭无痕模式);4k和杜比视界播放效果可能不佳',
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('选择画质', style: titleStyle),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(totalQaSam, (index) {
|
||||
final item = videoFormat[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
if (currentVideoQa.code == item.quality) {
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
final int quality = item.quality!;
|
||||
final newQa = VideoQuality.fromCode(quality);
|
||||
videoDetailCtr
|
||||
..currentVideoQa.value = newQa
|
||||
..updatePlayer();
|
||||
SliverList.builder(
|
||||
itemCount: totalQaSam,
|
||||
itemBuilder: (context, index) {
|
||||
final item = videoFormat[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
if (currentVideoQa.code == item.quality) {
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
final int quality = item.quality!;
|
||||
final newQa = VideoQuality.fromCode(quality);
|
||||
videoDetailCtr
|
||||
..currentVideoQa.value = newQa
|
||||
..updatePlayer();
|
||||
|
||||
SmartDialog.showToast("画质已变为:${newQa.desc}");
|
||||
SmartDialog.showToast("画质已变为:${newQa.desc}");
|
||||
|
||||
// update
|
||||
if (!plPlayerController.tempPlayerConf) {
|
||||
setting.put(
|
||||
await Utils.isWiFi
|
||||
? SettingBoxKey.defaultVideoQa
|
||||
: SettingBoxKey.defaultVideoQaCellular,
|
||||
quality,
|
||||
);
|
||||
}
|
||||
},
|
||||
// 可能包含会员解锁画质
|
||||
enabled: index >= totalQaSam - userfulQaSam,
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
title: Text(item.newDesc!),
|
||||
trailing: currentVideoQa.code == item.quality
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: Text(
|
||||
item.format!,
|
||||
style: subTitleStyle,
|
||||
),
|
||||
);
|
||||
}),
|
||||
// update
|
||||
if (!plPlayerController.tempPlayerConf) {
|
||||
setting.put(
|
||||
await Utils.isWiFi
|
||||
? SettingBoxKey.defaultVideoQa
|
||||
: SettingBoxKey.defaultVideoQaCellular,
|
||||
quality,
|
||||
);
|
||||
}
|
||||
},
|
||||
// 可能包含会员解锁画质
|
||||
enabled: index >= totalQaSam - userfulQaSam,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
title: Text(item.newDesc!),
|
||||
trailing: currentVideoQa.code == item.quality
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: Text(
|
||||
item.format!,
|
||||
style: subTitleStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -734,55 +740,62 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text('选择音质', style: titleStyle),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text('选择音质', style: titleStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final AudioItem i in audio) ...[
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
if (currentAudioQa.code == i.id) {
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
final int quality = i.id!;
|
||||
final newQa = AudioQuality.fromCode(quality);
|
||||
videoDetailCtr
|
||||
..currentAudioQa = newQa
|
||||
..updatePlayer();
|
||||
SliverList.builder(
|
||||
itemCount: audio.length,
|
||||
itemBuilder: (context, index) {
|
||||
final i = audio[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
if (currentAudioQa.code == i.id) {
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
final int quality = i.id!;
|
||||
final newQa = AudioQuality.fromCode(quality);
|
||||
videoDetailCtr
|
||||
..currentAudioQa = newQa
|
||||
..updatePlayer();
|
||||
|
||||
SmartDialog.showToast("音质已变为:${newQa.desc}");
|
||||
SmartDialog.showToast("音质已变为:${newQa.desc}");
|
||||
|
||||
// update
|
||||
if (!plPlayerController.tempPlayerConf) {
|
||||
setting.put(
|
||||
await Utils.isWiFi
|
||||
? SettingBoxKey.defaultAudioQa
|
||||
: SettingBoxKey.defaultAudioQaCellular,
|
||||
quality,
|
||||
);
|
||||
}
|
||||
},
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
title: Text(i.quality),
|
||||
subtitle: Text(
|
||||
i.codecs!,
|
||||
style: subTitleStyle,
|
||||
),
|
||||
trailing: currentAudioQa.code == i.id
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
// update
|
||||
if (!plPlayerController.tempPlayerConf) {
|
||||
setting.put(
|
||||
await Utils.isWiFi
|
||||
? SettingBoxKey.defaultAudioQa
|
||||
: SettingBoxKey.defaultAudioQaCellular,
|
||||
quality,
|
||||
);
|
||||
}
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
title: Text(i.quality),
|
||||
subtitle: Text(
|
||||
i.codecs!,
|
||||
style: subTitleStyle,
|
||||
),
|
||||
trailing: currentAudioQa.code == i.id
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -825,41 +838,41 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
for (var i in list) ...[
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
if (currentDecodeFormats.codes.any(i.startsWith)) {
|
||||
return;
|
||||
}
|
||||
videoDetailCtr
|
||||
..currentDecodeFormats =
|
||||
VideoDecodeFormatType.fromString(i)
|
||||
..updatePlayer();
|
||||
Get.back();
|
||||
},
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
),
|
||||
title: Text(
|
||||
VideoDecodeFormatType.fromString(i).description,
|
||||
),
|
||||
subtitle: Text(
|
||||
i,
|
||||
style: subTitleStyle,
|
||||
),
|
||||
trailing: currentDecodeFormats.codes.any(i.startsWith)
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.builder(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
final i = list[index];
|
||||
final format = VideoDecodeFormatType.fromString(i);
|
||||
return ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
if (currentDecodeFormats.codes.any(
|
||||
i.startsWith,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
videoDetailCtr
|
||||
..currentDecodeFormats = format
|
||||
..updatePlayer();
|
||||
Get.back();
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
title: Text(format.description),
|
||||
subtitle: Text(i, style: subTitleStyle),
|
||||
trailing:
|
||||
currentDecodeFormats.codes.any(i.startsWith)
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1845,32 +1858,39 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text('选择播放顺序', style: titleStyle),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text('选择播放顺序', style: titleStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final PlayRepeat i in PlayRepeat.values) ...[
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
plPlayerController.setPlayRepeat(i);
|
||||
Get.back();
|
||||
},
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
title: Text(i.desc),
|
||||
trailing: plPlayerController.playRepeat == i
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
SliverList.builder(
|
||||
itemCount: PlayRepeat.values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final i = PlayRepeat.values[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
plPlayerController.setPlayRepeat(i);
|
||||
Get.back();
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
title: Text(i.desc),
|
||||
trailing: plPlayerController.playRepeat == i
|
||||
? Icon(
|
||||
Icons.done,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user