mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-30 23:58:13 +08:00
tweaks (#1187)
* opt: marquee * fix: bangumi seek * opt: post panel * opt: remove deprecated code * opt: singleton dynController * fix: music scheme * feat: MemberVideo jump keep position * tweak
This commit is contained in:
committed by
GitHub
parent
e8a674ca2a
commit
172389b12b
@@ -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());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<void> autoWrapReportDialog(
|
||||
BuildContext context,
|
||||
Map<String, Map<int, String>> options,
|
||||
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
@@ -14,30 +14,30 @@ void autoWrapReportDialog(
|
||||
String? reasonDesc;
|
||||
bool banUid = false;
|
||||
late final key = GlobalKey<FormState>();
|
||||
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<int>(
|
||||
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<int>(
|
||||
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<CheckBoxText> {
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selected = !_selected;
|
||||
widget.onChanged(_selected);
|
||||
});
|
||||
widget.onChanged(_selected);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
|
||||
@@ -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<void> showMemberReportDialog(
|
||||
BuildContext context, {
|
||||
required Object? name,
|
||||
required Object mid,
|
||||
}) {
|
||||
final List<bool> reasonList = List.generate(3, (_) => false);
|
||||
final Set<int> reason = {};
|
||||
int? reasonV2;
|
||||
|
||||
final dynamic name;
|
||||
final dynamic mid;
|
||||
|
||||
@override
|
||||
State<MemberReportPanel> createState() => _MemberReportPanelState();
|
||||
}
|
||||
|
||||
class _MemberReportPanelState extends State<MemberReportPanel> {
|
||||
final List<bool> _reasonList = List.generate(3, (_) => false);
|
||||
final Set<int> _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<int>(
|
||||
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<int>(
|
||||
onChanged: (v) {
|
||||
reasonV2 = v;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonV2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) => RadioListTile<int>(
|
||||
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('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(<DiagnosticsNode>[
|
||||
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<bool>? _tickerModeNotifier;
|
||||
|
||||
void updateTicker() => _ticker?.muted = !_tickerModeNotifier!.value;
|
||||
|
||||
set muted(bool value) => _ticker?.muted = value;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RadioWidget<T> extends StatelessWidget {
|
||||
class RadioWidget<T> extends StatefulWidget {
|
||||
final T value;
|
||||
final T? groupValue;
|
||||
final ValueChanged<T?> 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<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
Text(title),
|
||||
],
|
||||
);
|
||||
@override
|
||||
State<RadioWidget<T>> createState() => RadioWidgetState<T>();
|
||||
}
|
||||
|
||||
class RadioWidgetState<T> extends State<RadioWidget<T>> with RadioClient<T> {
|
||||
late final _RadioRegistry<T> _radioRegistry = _RadioRegistry<T>(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<T>(
|
||||
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<T> extends StatelessWidget {
|
||||
class WrapRadioOptionsGroup<T> extends StatelessWidget {
|
||||
final String groupTitle;
|
||||
final Map<T, String> options;
|
||||
final T? selectedValue;
|
||||
final ValueChanged<T?> 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<T> extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Wrap(
|
||||
children: options.entries.map((entry) {
|
||||
return IntrinsicWidth(
|
||||
child: RadioWidget<T>(
|
||||
value: entry.key,
|
||||
groupValue: selectedValue,
|
||||
onChanged: onChanged,
|
||||
title: entry.value,
|
||||
padding: itemPadding ?? const EdgeInsets.only(right: 10),
|
||||
),
|
||||
return RadioWidget<T>(
|
||||
value: entry.key,
|
||||
title: entry.value,
|
||||
padding: itemPadding ?? const EdgeInsets.only(right: 10),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -91,3 +128,27 @@ class WrapRadioOptionsGroup<T> 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<T> extends RadioGroupRegistry<T> {
|
||||
_RadioRegistry(this.state);
|
||||
|
||||
final RadioWidgetState<T> state;
|
||||
|
||||
@override
|
||||
T? get groupValue => state.registry!.groupValue;
|
||||
|
||||
@override
|
||||
ValueChanged<T?> get onChanged => state.registry!.onChanged;
|
||||
|
||||
@override
|
||||
void registerClient(RadioClient<T> radio) {}
|
||||
|
||||
@override
|
||||
void unregisterClient(RadioClient<T> radio) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user