* 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:
My-Responsitories
2025-09-04 20:29:02 +08:00
committed by GitHub
parent e8a674ca2a
commit 172389b12b
51 changed files with 1314 additions and 1227 deletions

View File

@@ -15,7 +15,7 @@ class ColorPalette extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(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 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 tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt()); final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());

View File

@@ -46,7 +46,7 @@ class LoadingWidget extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.dialogBackgroundColor, color: theme.dialogTheme.backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(15)), borderRadius: const BorderRadius.all(Radius.circular(15)),
), ),
child: Column( child: Column(

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
void autoWrapReportDialog( Future<void> autoWrapReportDialog(
BuildContext context, BuildContext context,
Map<String, Map<int, String>> options, Map<String, Map<int, String>> options,
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid) Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
@@ -14,30 +14,30 @@ void autoWrapReportDialog(
String? reasonDesc; String? reasonDesc;
bool banUid = false; bool banUid = false;
late final key = GlobalKey<FormState>(); late final key = GlobalKey<FormState>();
showDialog( return showDialog(
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (context) {
builder: (context, setState) { return AlertDialog(
return AlertDialog( title: const Text('举报'),
title: const Text('举报'), titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22), contentPadding: const EdgeInsets.symmetric(vertical: 5),
contentPadding: const EdgeInsets.symmetric(vertical: 5), actionsPadding: const EdgeInsets.only(
actionsPadding: const EdgeInsets.only( left: 16,
left: 16, right: 16,
right: 16, bottom: 10,
bottom: 10, ),
), content: Form(
content: Form( key: key,
key: key, child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Flexible(
Flexible( child: SingleChildScrollView(
child: SingleChildScrollView( child: AnimatedSize(
child: AnimatedSize( duration: const Duration(milliseconds: 200),
duration: const Duration(milliseconds: 200), child: Builder(
child: Column( builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding( const Padding(
@@ -48,13 +48,20 @@ void autoWrapReportDialog(
), ),
child: Text('请选择举报的理由:'), child: Text('请选择举报的理由:'),
), ),
...options.entries.map( RadioGroup(
(entry) => WrapRadioOptionsGroup<int>( onChanged: (value) {
groupTitle: entry.key, reasonType = value;
options: entry.value, (context as Element).markNeedsBuild();
selectedValue: reasonType, },
onChanged: (value) => groupValue: reasonType,
setState(() => reasonType = value), child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: options.entries.map((entry) {
return WrapRadioOptionsGroup<int>(
groupTitle: entry.key,
options: entry.value,
);
}).toList(),
), ),
), ),
if (reasonType == 0) if (reasonType == 0)
@@ -66,51 +73,51 @@ void autoWrapReportDialog(
), ),
), ),
), ),
Padding( ),
padding: const EdgeInsets.only(left: 14, top: 6), Padding(
child: CheckBoxText( padding: const EdgeInsets.only(left: 14, top: 6),
text: '拉黑该用户', child: CheckBoxText(
onChanged: (value) => banUid = value, text: '拉黑该用户',
), onChanged: (value) => banUid = value,
), ),
], ),
],
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
), ),
), ),
actions: [ TextButton(
TextButton( onPressed: () async {
onPressed: Get.back, if (reasonType == null ||
child: Text( (reasonType == 0 && key.currentState?.validate() != true)) {
'取消', return;
style: TextStyle(color: Theme.of(context).colorScheme.outline), }
), SmartDialog.showLoading();
), try {
TextButton( final data = await onSuccess(reasonType!, reasonDesc, banUid);
onPressed: () async { SmartDialog.dismiss();
if (reasonType == null || if (data['code'] == 0) {
(reasonType == 0 && key.currentState?.validate() != true)) { Get.back();
return; SmartDialog.showToast('举报成功');
} else {
SmartDialog.showToast(data['message']);
} }
SmartDialog.showLoading(); } catch (e) {
try { SmartDialog.dismiss();
final data = await onSuccess(reasonType!, reasonDesc, banUid); SmartDialog.showToast('提交失败:$e');
SmartDialog.dismiss(); }
if (data['code'] == 0) { },
Get.back(); child: const Text('确定'),
SmartDialog.showToast('举报成功'); ),
} else { ],
SmartDialog.showToast(data['message']); );
} },
} catch (e) {
SmartDialog.dismiss();
SmartDialog.showToast('提交失败:$e');
}
},
child: const Text('确定'),
),
],
);
},
),
); );
} }
@@ -186,8 +193,8 @@ class _CheckBoxTextState extends State<CheckBoxText> {
onTap: () { onTap: () {
setState(() { setState(() {
_selected = !_selected; _selected = !_selected;
widget.onChanged(_selected);
}); });
widget.onChanged(_selected);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),

View File

@@ -1,126 +1,124 @@
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/member.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class MemberReportPanel extends StatefulWidget { Future<void> showMemberReportDialog(
const MemberReportPanel({ BuildContext context, {
super.key, required Object? name,
required this.name, required Object mid,
required this.mid, }) {
}); final List<bool> reasonList = List.generate(3, (_) => false);
final Set<int> reason = {};
int? reasonV2;
final dynamic name; return showDialog(
final dynamic mid; context: context,
builder: (context) {
@override final theme = Theme.of(context);
State<MemberReportPanel> createState() => _MemberReportPanelState(); return AlertDialog(
} clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(
class _MemberReportPanelState extends State<MemberReportPanel> { horizontal: 20,
final List<bool> _reasonList = List.generate(3, (_) => false); vertical: 16,
final Set<int> _reason = {}; ),
int? _reasonV2; titleTextStyle: theme.textTheme.bodyMedium,
title: Column(
@override spacing: 4,
Widget build(BuildContext context) { children: [
final theme = Theme.of(context); Text(
return SingleChildScrollView( '举报: $name',
child: Column( style: const TextStyle(fontSize: 18),
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],
), ),
), Text('uid: $mid'),
const Text('举报理由(单选,非必选)'), ],
...List.generate( ),
5, content: SingleChildScrollView(
(index) => RadioWidget<int>( child: Column(
value: index, mainAxisSize: MainAxisSize.min,
groupValue: _reasonV2, crossAxisAlignment: CrossAxisAlignment.start,
onChanged: (value) {
setState(() => _reasonV2 = value);
},
title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( const Text('举报内容(必选,可多选)'),
onPressed: Get.back, ...List.generate(
child: Text( 3,
'取消', (index) => Builder(
style: TextStyle(color: theme.colorScheme.outline), 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( const Text('举报理由(单选,非必选)'),
onPressed: () async { Builder(
if (_reason.isEmpty) { builder: (context) => RadioGroup<int>(
SmartDialog.showToast('至少选择一项作为举报内容'); onChanged: (v) {
} else { reasonV2 = v;
Get.back(); (context as Element).markNeedsBuild();
var result = await MemberHttp.reportMember( },
widget.mid, groupValue: reasonV2,
reason: _reason.join(','), child: Column(
reasonV2: _reasonV2 != null ? _reasonV2! + 1 : null, crossAxisAlignment: CrossAxisAlignment.start,
); children: List.generate(
if (result['msg'] is String && result['msg'].isNotEmpty) { 5,
SmartDialog.showToast(result['msg']); (index) => RadioListTile<int>(
} else { toggleable: true,
SmartDialog.showToast('举报失败'); controlAffinity: ListTileControlAffinity.leading,
} contentPadding: const EdgeInsets.only(left: 4),
} dense: true,
}, value: index,
child: const Text('确定'), 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('确定'),
),
],
);
},
); );
} }

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@@ -7,7 +8,7 @@ class MarqueeText extends StatelessWidget {
final TextStyle? style; final TextStyle? style;
final double spacing; final double spacing;
final double velocity; final double velocity;
final MarqueeController? controller; final ContextSingleTicker? provider;
const MarqueeText( const MarqueeText(
this.text, { this.text, {
@@ -15,7 +16,7 @@ class MarqueeText extends StatelessWidget {
this.style, this.style,
this.spacing = 0, this.spacing = 0,
this.velocity = 25, this.velocity = 25,
this.controller, this.provider,
}); });
@override @override
@@ -23,7 +24,7 @@ class MarqueeText extends StatelessWidget {
return NormalMarquee( return NormalMarquee(
velocity: velocity, velocity: velocity,
spacing: spacing, spacing: spacing,
controller: controller, provider: provider,
child: Text( child: Text(
text, text,
style: style, style: style,
@@ -39,7 +40,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget {
final Clip clipBehavior; final Clip clipBehavior;
final double spacing; final double spacing;
final double velocity; final double velocity;
final MarqueeController? controller; final ContextSingleTicker? provider;
const Marquee({ const Marquee({
super.key, super.key,
@@ -48,7 +49,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget {
this.direction = Axis.horizontal, this.direction = Axis.horizontal,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.spacing = 0, this.spacing = 0,
this.controller, this.provider,
}); });
@override @override
@@ -61,6 +62,10 @@ abstract class Marquee extends SingleChildRenderObjectWidget {
..clipBehavior = clipBehavior ..clipBehavior = clipBehavior
..velocity = velocity ..velocity = velocity
..spacing = spacing; ..spacing = spacing;
if (provider != null) {
renderObject.provider = provider!;
}
} }
} }
@@ -72,7 +77,7 @@ class NormalMarquee extends Marquee {
super.direction, super.direction,
super.clipBehavior, super.clipBehavior,
super.spacing, super.spacing,
super.controller, super.provider,
}); });
@override @override
@@ -81,7 +86,7 @@ class NormalMarquee extends Marquee {
velocity: velocity, velocity: velocity,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
spacing: spacing, spacing: spacing,
controller: controller, provider: provider ?? ContextSingleTicker(context),
); );
} }
@@ -93,6 +98,7 @@ class BounceMarquee extends Marquee {
super.direction, super.direction,
super.clipBehavior, super.clipBehavior,
super.spacing, super.spacing,
super.provider,
}); });
@override @override
@@ -101,6 +107,7 @@ class BounceMarquee extends Marquee {
velocity: velocity, velocity: velocity,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
spacing: spacing, spacing: spacing,
provider: provider ?? ContextSingleTicker(context),
); );
} }
@@ -111,16 +118,15 @@ abstract class MarqueeRender extends RenderBox
required double velocity, required double velocity,
required double spacing, required double spacing,
required this.clipBehavior, required this.clipBehavior,
this.controller, required ContextSingleTicker provider,
}) : _spacing = spacing, }) : _ticker = provider,
_spacing = spacing,
_velocity = velocity, _velocity = velocity,
_direction = direction, _direction = direction,
assert(spacing.isFinite && !spacing.isNaN); assert(spacing.isFinite && !spacing.isNaN);
Clip clipBehavior; Clip clipBehavior;
MarqueeController? controller;
Axis _direction; Axis _direction;
Axis get direction => _direction; Axis get direction => _direction;
set direction(Axis value) { set direction(Axis value) {
@@ -129,12 +135,26 @@ abstract class MarqueeRender extends RenderBox
markNeedsLayout(); 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; double _velocity;
set velocity(double value) { set velocity(double value) {
if (_velocity == value) return; if (_velocity == value) return;
_velocity = value; _velocity = value;
_simulation = _simulation?.copyWith(initialValue: _delta, velocity: value); _simulation = _simulation?.copyWith(initialValue: _delta, velocity: value);
controller?.reset(); _ticker.reset();
} }
double _spacing; double _spacing;
@@ -149,7 +169,7 @@ abstract class MarqueeRender extends RenderBox
addSize: value - _spacing, addSize: value - _spacing,
); );
_spacing = value; _spacing = value;
controller?.reset(); _ticker.reset();
} }
double _delta = 0; double _delta = 0;
@@ -160,14 +180,14 @@ abstract class MarqueeRender extends RenderBox
} }
@override @override
void detach() { void attach(PipelineOwner owner) {
controller?.dispose(); super.attach(owner);
super.detach(); _ticker.updateTicker();
} }
@override @override
void dispose() { void dispose() {
controller?.dispose(); _ticker.cancel();
super.dispose(); super.dispose();
} }
@@ -203,11 +223,9 @@ abstract class MarqueeRender extends RenderBox
if (_distance > 0) { if (_distance > 0) {
updateSize(); updateSize();
(controller ??= MarqueeController()) _ticker.createTicker(_onTick);
..ticker ??= Ticker(_onTick)
..initStart();
} else { } else {
controller?.dispose(); _ticker.cancel();
} }
} }
@@ -237,6 +255,7 @@ class _BounceMarqueeRender extends MarqueeRender {
required super.velocity, required super.velocity,
required super.clipBehavior, required super.clipBehavior,
required super.spacing, required super.spacing,
required super.provider,
}); });
@override @override
@@ -278,7 +297,7 @@ class _NormalMarqueeRender extends MarqueeRender {
required super.velocity, required super.velocity,
required super.clipBehavior, required super.clipBehavior,
required super.spacing, required super.spacing,
super.controller, required super.provider,
}); });
@override @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() { void reset() {
this _ticker
..stop() ?..stop()
..start(); ..start();
} }
}
void cancel() {
class MarqueeController { _ticker?.dispose();
MarqueeController({this.autoStart = true}); _ticker = null;
bool autoStart; _tickerModeNotifier?.removeListener(updateTicker);
_tickerModeNotifier = null;
Ticker? ticker; }
void initStart() { ValueListenable<bool>? _tickerModeNotifier;
if (autoStart) {
start(); void updateTicker() => _ticker?.muted = !_tickerModeNotifier!.value;
}
} set muted(bool value) => _ticker?.muted = value;
void start() {
if (ticker != null) {
if (!ticker!.isTicking) {
ticker!.start();
}
}
}
void stop() {
ticker?.stop();
}
void reset() {
ticker?.reset();
}
void dispose() {
ticker?.dispose();
ticker = null;
}
} }

View File

@@ -1,43 +1,88 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class RadioWidget<T> extends StatelessWidget { class RadioWidget<T> extends StatefulWidget {
final T value; final T value;
final T? groupValue;
final ValueChanged<T?> onChanged;
final String title; final String title;
final bool tristate;
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final MainAxisSize mainAxisSize;
const RadioWidget({ const RadioWidget({
super.key, super.key,
required this.value, required this.value,
this.groupValue,
required this.onChanged,
required this.title, required this.title,
this.tristate = false,
this.padding, this.padding,
this.mainAxisSize = MainAxisSize.min,
}); });
Widget _child() => Row( @override
children: [ State<RadioWidget<T>> createState() => RadioWidgetState<T>();
Radio<T>( }
value: value,
groupValue: groupValue, class RadioWidgetState<T> extends State<RadioWidget<T>> with RadioClient<T> {
onChanged: onChanged, late final _RadioRegistry<T> _radioRegistry = _RadioRegistry<T>(this);
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
), @override
Text(title), 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 @override
Widget build(BuildContext context) { 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( return InkWell(
onTap: () => onChanged(value), onTap: _handleTap,
child: padding != null focusNode: focusNode,
? Padding( child: widget.padding == null
padding: padding!, ? child
child: _child(), : Padding(padding: widget.padding!, child: child),
)
: _child(),
); );
} }
} }
@@ -45,16 +90,12 @@ class RadioWidget<T> extends StatelessWidget {
class WrapRadioOptionsGroup<T> extends StatelessWidget { class WrapRadioOptionsGroup<T> extends StatelessWidget {
final String groupTitle; final String groupTitle;
final Map<T, String> options; final Map<T, String> options;
final T? selectedValue;
final ValueChanged<T?> onChanged;
final EdgeInsetsGeometry? itemPadding; final EdgeInsetsGeometry? itemPadding;
const WrapRadioOptionsGroup({ const WrapRadioOptionsGroup({
super.key, super.key,
required this.groupTitle, required this.groupTitle,
required this.options, required this.options,
required this.selectedValue,
required this.onChanged,
this.itemPadding, this.itemPadding,
}); });
@@ -75,14 +116,10 @@ class WrapRadioOptionsGroup<T> extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap( child: Wrap(
children: options.entries.map((entry) { children: options.entries.map((entry) {
return IntrinsicWidth( return RadioWidget<T>(
child: RadioWidget<T>( value: entry.key,
value: entry.key, title: entry.value,
groupValue: selectedValue, padding: itemPadding ?? const EdgeInsets.only(right: 10),
onChanged: onChanged,
title: entry.value,
padding: itemPadding ?? const EdgeInsets.only(right: 10),
),
); );
}).toList(), }).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) {}
}

View File

@@ -212,7 +212,7 @@ class DynamicsHttp {
} }
// //
static Future dynamicDetail({ static Future<LoadingState<DynamicItemModel>> dynamicDetail({
dynamic id, dynamic id,
dynamic rid, dynamic rid,
dynamic type, dynamic type,
@@ -236,21 +236,12 @@ class DynamicsHttp {
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
try { try {
return { return Success(DynamicItemModel.fromJson(res.data['data']['item']));
'status': true,
'data': DynamicItemModel.fromJson(res.data['data']['item']),
};
} catch (err) { } catch (err) {
return { return Error(err.toString());
'status': false,
'msg': err.toString(),
};
} }
} else { } else {
return { return Error(res.data['message']);
'status': false,
'msg': res.data['message'],
};
} }
} }

View File

@@ -118,7 +118,7 @@ class MemberHttp {
int? next, int? next,
int? seasonId, int? seasonId,
int? seriesId, int? seriesId,
includeCursor, bool? includeCursor,
}) async { }) async {
final params = { final params = {
'aid': ?aid, 'aid': ?aid,

View File

@@ -33,38 +33,31 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
await GStorage.init(); await GStorage.init();
Get.put(AccountService()); Get.lazyPut(AccountService.new);
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,
],
);
}
HttpOverrides.global = _CustomHttpOverrides(); 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();
Request.setCookie(); Request.setCookie();

View File

@@ -56,9 +56,16 @@ class _AboutPageState extends State<AboutPage> {
getCurrentApp(); getCurrentApp();
} }
@override
void dispose() {
currentVersion.close();
cacheSize.close();
super.dispose();
}
Future<void> getCacheSize() async { Future<void> getCacheSize() async {
cacheSize.value = CacheManage.formatSize( cacheSize.value = CacheManage.formatSize(
await CacheManage().loadApplicationCache(), await CacheManage.loadApplicationCache(),
); );
} }

View File

@@ -63,6 +63,7 @@ class ArticleController extends CommonDynController {
id = opusId; id = opusId;
type = 'opus'; type = 'opus';
} }
Get.putOrFind(() => this, tag: type + id);
} }
init(); init();
}); });

View File

@@ -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/dynamics_repost/view.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/date_util.dart'; import 'package:PiliPlus/utils/date_util.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/image_util.dart'; import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/num_util.dart'; import 'package:PiliPlus/utils/num_util.dart';
@@ -42,9 +43,9 @@ class ArticlePage extends StatefulWidget {
class _ArticlePageState extends CommonDynPageState<ArticlePage> { class _ArticlePageState extends CommonDynPageState<ArticlePage> {
@override @override
final ArticleController controller = Get.put( final ArticleController controller = Get.putOrFind(
ArticleController(), ArticleController.new,
tag: Utils.generateRandomString(8), tag: Get.parameters['type']! + Get.parameters['id']!,
); );
@override @override
@@ -56,9 +57,9 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.scrollController.hasClients) { if (scrollController.hasClients) {
controller.showTitle.value = controller.showTitle.value =
controller.scrollController.positions.last.pixels >= 45; scrollController.positions.last.pixels >= 45;
} }
}); });
} }
@@ -88,7 +89,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: padding), padding: EdgeInsets.symmetric(horizontal: padding),
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
_buildContent( _buildContent(
@@ -117,7 +118,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
Expanded( Expanded(
flex: flex, flex: flex,
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverPadding( SliverPadding(
@@ -150,7 +151,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
body: refreshIndicator( body: refreshIndicator(
onRefresh: controller.onRefresh, onRefresh: controller.onRefresh,
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
buildReplyHeader(theme), buildReplyHeader(theme),
@@ -554,7 +555,7 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
bottom: 0, bottom: 0,
right: 0, right: 0,
child: SlideTransition( child: SlideTransition(
position: controller.fabAnim, position: fabAnim,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (!controller.showDynActionBar) { if (!controller.showDynActionBar) {

View File

@@ -3,84 +3,17 @@ import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:PiliPlus/utils/storage_pref.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'; import 'package:get/get.dart';
abstract class CommonDynController extends ReplyController<MainListReply> abstract class CommonDynController extends ReplyController<MainListReply> {
with GetSingleTickerProviderStateMixin {
int get oid; int get oid;
int get replyType; int get replyType;
bool _showFab = true;
late final AnimationController fabAnimationCtr;
late final Animation<Offset> fabAnim;
late final RxBool showTitle = false.obs; late final RxBool showTitle = false.obs;
late final horizontalPreview = Pref.horizontalPreview; late final horizontalPreview = Pref.horizontalPreview;
late final List<double> ratio = Pref.dynamicDetailRatio; late final List<double> ratio = Pref.dynamicDetailRatio;
final fabOffset = const Offset(0, 1);
@override
void onInit() {
fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
fabAnim =
Tween<Offset>(
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 @override
Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList( Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList(
type: replyType, type: replyType,

View File

@@ -18,12 +18,15 @@ import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_key.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart' hide ContextExtensionss; import 'package:get/get.dart' hide ContextExtensionss;
abstract class CommonDynPageState<T extends StatefulWidget> extends State<T> abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
with TickerProviderStateMixin { with TickerProviderStateMixin {
CommonDynController get controller; CommonDynController get controller;
late final scrollController = ScrollController()..addListener(listener);
late final scaffoldKey = GlobalKey<ScaffoldState>(); late final scaffoldKey = GlobalKey<ScaffoldState>();
bool get horizontalPreview => !isPortrait && controller.horizontalPreview; bool get horizontalPreview => !isPortrait && controller.horizontalPreview;
@@ -35,6 +38,49 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
late bool isPortrait; late bool isPortrait;
late double maxWidth; 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<Offset> fabAnim = Tween<Offset>(
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 @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -43,7 +89,7 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
isPortrait = size.isPortrait; isPortrait = size.isPortrait;
imageCallback = horizontalPreview imageCallback = horizontalPreview
? (imgList, index) { ? (imgList, index) {
controller.hideFab(); hideFab();
PageUtils.onHorizontalPreview( PageUtils.onHorizontalPreview(
scaffoldKey, scaffoldKey,
this, this,
@@ -55,6 +101,12 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
padding = MediaQuery.viewPaddingOf(context); padding = MediaQuery.viewPaddingOf(context);
} }
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
Widget buildReplyHeader(ThemeData theme) { Widget buildReplyHeader(ThemeData theme) {
final secondary = theme.colorScheme.secondary; final secondary = theme.colorScheme.secondary;
return SliverPersistentHeader( return SliverPersistentHeader(
@@ -220,7 +272,7 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
} else { } else {
ScaffoldState? scaffoldState = Scaffold.maybeOf(context); ScaffoldState? scaffoldState = Scaffold.maybeOf(context);
if (scaffoldState != null) { if (scaffoldState != null) {
controller.hideFab(); hideFab();
scaffoldState.showBottomSheet( scaffoldState.showBottomSheet(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
(context) => replyReplyPage(showBackBtn: false), (context) => replyReplyPage(showBackBtn: false),

View File

@@ -3,7 +3,6 @@ import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class DynamicDetailController extends CommonDynController { class DynamicDetailController extends CommonDynController {
@@ -30,11 +29,11 @@ class DynamicDetailController extends CommonDynController {
_init(commentIdStr!, commentType); _init(commentIdStr!, commentType);
} else { } else {
DynamicsHttp.dynamicDetail(id: dynItem.idStr).then((res) { DynamicsHttp.dynamicDetail(id: dynItem.idStr).then((res) {
if (res['status']) { if (res.isSuccess) {
DynamicItemModel data = res['data']; final data = res.data;
_init(data.basic!.commentIdStr!, data.basic!.commentType!); _init(data.basic!.commentIdStr!, data.basic!.commentType!);
} else { } else {
SmartDialog.showToast(res['msg']); res.toast();
} }
}); });
} }

View File

@@ -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/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/dynamics_detail/controller.dart'; import 'package:PiliPlus/pages/dynamics_detail/controller.dart';
import 'package:PiliPlus/pages/dynamics_repost/view.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/grid.dart';
import 'package:PiliPlus/utils/num_util.dart'; import 'package:PiliPlus/utils/num_util.dart';
import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart';
@@ -26,9 +27,9 @@ class DynamicDetailPage extends StatefulWidget {
class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> { class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
@override @override
final DynamicDetailController controller = Get.put( final DynamicDetailController controller = Get.putOrFind(
DynamicDetailController(), DynamicDetailController.new,
tag: Utils.generateRandomString(8), tag: (Get.arguments['item'] as DynamicItemModel).idStr.toString(),
); );
@override @override
@@ -40,9 +41,9 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.scrollController.hasClients) { if (scrollController.hasClients) {
controller.showTitle.value = controller.showTitle.value =
controller.scrollController.positions.first.pixels > 55; scrollController.positions.first.pixels > 55;
} }
}); });
} }
@@ -100,7 +101,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
child = Padding( child = Padding(
padding: EdgeInsets.symmetric(horizontal: padding), padding: EdgeInsets.symmetric(horizontal: padding),
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
@@ -126,7 +127,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
Expanded( Expanded(
flex: flex, flex: flex,
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverPadding( SliverPadding(
@@ -161,7 +162,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
body: refreshIndicator( body: refreshIndicator(
onRefresh: controller.onRefresh, onRefresh: controller.onRefresh,
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
buildReplyHeader(theme), buildReplyHeader(theme),
@@ -192,7 +193,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
right: 0, right: 0,
bottom: 0, bottom: 0,
child: SlideTransition( child: SlideTransition(
position: controller.fabAnim, position: fabAnim,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (!controller.showDynActionBar) { if (!controller.showDynActionBar) {
@@ -282,8 +283,8 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
int count = forward.count ?? 0; int count = forward.count ?? 0;
forward.count = count + 1; forward.count = count + 1;
if (btnContext.mounted) { if (btnContext.mounted) {
(btnContext as Element?) (btnContext as Element)
?.markNeedsBuild(); .markNeedsBuild();
} }
} }
}, },
@@ -315,7 +316,7 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
controller.dynItem, controller.dynItem,
() { () {
if (context.mounted) { if (context.mounted) {
(context as Element?)?.markNeedsBuild(); (context as Element).markNeedsBuild();
} }
}, },
), ),

View File

@@ -708,56 +708,59 @@ class LoginPageController extends GetxController
}; };
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (context) => AlertDialog(
builder: (context, setState) { title: const Text('选择账号mid, 为0时使用匿名'),
return AlertDialog( titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
title: const Text('选择账号mid, 为0时使用匿名'), contentPadding: const EdgeInsets.symmetric(vertical: 5),
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22), actionsPadding: const EdgeInsets.only(
contentPadding: const EdgeInsets.symmetric(vertical: 5), left: 16,
actionsPadding: const EdgeInsets.only( right: 16,
left: 16, bottom: 10,
right: 16, ),
bottom: 10, content: SingleChildScrollView(
), child: Column(
content: SingleChildScrollView( crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: AccountType.values
crossAxisAlignment: CrossAxisAlignment.start, .map(
children: AccountType.values (e) => Builder(
.map( builder: (context) => RadioGroup(
(e) => WrapRadioOptionsGroup<Account>( groupValue: selectAccount[e],
onChanged: (v) {
selectAccount[e] = v!;
(context as Element).markNeedsBuild();
},
child: WrapRadioOptionsGroup<Account>(
groupTitle: e.title, groupTitle: e.title,
options: options, 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( TextButton(
onPressed: Get.back, onPressed: () {
child: Text( for (var i in selectAccount.entries) {
'取消', if (i.value != Accounts.get(i.key)) {
style: TextStyle( Accounts.set(i.key, i.value);
color: Theme.of(context).colorScheme.outline, }
), }
), Get.back();
), },
TextButton( child: const Text('确定'),
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('确定'),
),
],
);
},
), ),
); );
} }

View File

@@ -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/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart';
import 'package:PiliPlus/utils/date_util.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/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -31,14 +31,17 @@ class MatchInfoPage extends StatefulWidget {
class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> { class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
@override @override
final MatchInfoController controller = Get.put( final MatchInfoController controller = Get.putOrFind(
MatchInfoController(), MatchInfoController.new,
tag: Utils.generateRandomString(8), tag: Get.parameters['cid']!,
); );
@override @override
dynamic get arguments => null; dynamic get arguments => null;
@override
Offset get fabOffset => const Offset(0, 2);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -48,7 +51,7 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
body: refreshIndicator( body: refreshIndicator(
onRefresh: controller.onRefresh, onRefresh: controller.onRefresh,
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
Obx(() => _buildInfo(theme, controller.infoState.value)), Obx(() => _buildInfo(theme, controller.infoState.value)),
@@ -61,7 +64,7 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
), ),
), ),
floatingActionButton: SlideTransition( floatingActionButton: SlideTransition(
position: controller.fabAnim, position: fabAnim,
child: replyButton, child: replyButton,
), ),
); );

View File

@@ -55,7 +55,7 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
@override @override
bool customHandleResponse(bool isRefresh, Success<SpaceData> response) { bool customHandleResponse(bool isRefresh, Success<SpaceData> response) {
SpaceData data = response.response; final data = response.response;
username = data.card?.name ?? ''; username = data.card?.name ?? '';
isFollowed = data.card?.relation?.isFollowed; isFollowed = data.card?.relation?.isFollowed;
if (data.relation == -1) { if (data.relation == -1) {
@@ -215,11 +215,7 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
} }
Future<void> onRemoveFan() async { Future<void> onRemoveFan() async {
final res = await VideoHttp.relationMod( final res = await VideoHttp.relationMod(mid: mid, act: 7, reSrc: 11);
mid: mid,
act: 7,
reSrc: 11,
);
if (res['status']) { if (res['status']) {
isFollowed = null; isFollowed = null;
if (relation.value == 4) { if (relation.value == 4) {

View File

@@ -58,7 +58,6 @@ class _MemberPageState extends State<MemberPage> {
if (_userController.loadingState.value.isSuccess) { if (_userController.loadingState.value.isSuccess) {
return ExtendedNestedScrollView( return ExtendedNestedScrollView(
key: _userController.key, key: _userController.key,
controller: _userController.scrollController,
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
pinnedHeaderSliverHeightBuilder: () => pinnedHeaderSliverHeightBuilder: () =>
kToolbarHeight + MediaQuery.viewPaddingOf(context).top, kToolbarHeight + MediaQuery.viewPaddingOf(context).top,
@@ -259,19 +258,10 @@ class _MemberPageState extends State<MemberPage> {
] else ...[ ] else ...[
const PopupMenuDivider(), const PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
onTap: () => showDialog( onTap: () => showMemberReportDialog(
context: context, context,
builder: (context) => AlertDialog( name: _userController.username,
clipBehavior: Clip.hardEdge, mid: _mid,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
content: MemberReportPanel(
name: _userController.username,
mid: _mid,
),
),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@@ -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/image_save.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/stat/stat.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/common/stat_type.dart';
import 'package:PiliPlus/models_new/space/space_audio/item.dart'; import 'package:PiliPlus/models_new/space/space_audio/item.dart';
import 'package:PiliPlus/utils/date_util.dart'; import 'package:PiliPlus/utils/date_util.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class MemberAudioItem extends StatelessWidget { class MemberAudioItem extends StatelessWidget {
const MemberAudioItem({super.key, required this.item}); const MemberAudioItem({super.key, required this.item});
@@ -19,8 +22,18 @@ class MemberAudioItem extends StatelessWidget {
return Material( return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
onTap: () { onTap: () async {
// TODO // 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: () => onLongPress: () =>
imageSaveDialog(title: item.title, cover: item.cover), imageSaveDialog(title: item.title, cover: item.cover),

View File

@@ -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_opus/view.dart';
import 'package:PiliPlus/pages/member_season_series/view.dart'; import 'package:PiliPlus/pages/member_season_series/view.dart';
import 'package:PiliPlus/pages/member_video/view.dart'; import 'package:PiliPlus/pages/member_video/view.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -31,8 +32,8 @@ class _MemberContributeState extends State<MemberContribute>
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
late final _controller = Get.put( late final _controller = Get.putOrFind(
MemberContributeCtr( () => MemberContributeCtr(
heroTag: widget.heroTag, heroTag: widget.heroTag,
initialIndex: widget.initialIndex, initialIndex: widget.initialIndex,
), ),

View File

@@ -40,18 +40,19 @@ class MemberVideoCtr
String? firstAid; String? firstAid;
String? lastAid; String? lastAid;
String? fromViewAid; String? fromViewAid;
Rx<bool?> isLocating = Rx<bool?>(null); RxBool isLocating = false.obs;
bool? isLoadPrevious; bool isLoadPrevious = false;
bool? hasPrev; bool? hasPrev;
@override @override
Future<void> onRefresh() async { Future<void> onRefresh() async {
if (isLocating.value == true) { if (isLocating.value) {
if (hasPrev == true) { if (hasPrev == true) {
isLoadPrevious = true; isLoadPrevious = true;
await queryData(); await queryData();
} }
} else { } else {
isLoadPrevious = false;
firstAid = null; firstAid = null;
lastAid = null; lastAid = null;
next = null; next = null;
@@ -76,15 +77,15 @@ class MemberVideoCtr
bool isRefresh, bool isRefresh,
Success<SpaceArchiveData> response, Success<SpaceArchiveData> response,
) { ) {
SpaceArchiveData data = response.response; final data = response.response;
episodicButton episodicButton
..value = data.episodicButton ?? EpisodicButton() ..value = data.episodicButton ?? EpisodicButton()
..refresh(); ..refresh();
next = data.next; next = data.next;
if (page == 0 || isLoadPrevious == true) { if (page == 0 || isLoadPrevious) {
hasPrev = data.hasPrev; hasPrev = data.hasPrev;
} }
if (page == 0 || isLoadPrevious != true) { if (page == 0 || !isLoadPrevious) {
if ((type == ContributeType.video if ((type == ContributeType.video
? data.hasNext == false ? data.hasNext == false
: data.next == 0) || : data.next == 0) ||
@@ -97,7 +98,7 @@ class MemberVideoCtr
: (data.count ?? -1); : (data.count ?? -1);
if (page != 0 && loadingState.value.isSuccess) { if (page != 0 && loadingState.value.isSuccess) {
data.item ??= <SpaceArchiveItem>[]; data.item ??= <SpaceArchiveItem>[];
if (isLoadPrevious == true) { if (isLoadPrevious) {
data.item!.addAll(loadingState.value.data!); data.item!.addAll(loadingState.value.data!);
} else { } else {
data.item!.insertAll(0, loadingState.value.data!); data.item!.insertAll(0, loadingState.value.data!);
@@ -105,7 +106,6 @@ class MemberVideoCtr
} }
firstAid = data.item?.firstOrNull?.param; firstAid = data.item?.firstOrNull?.param;
lastAid = data.item?.lastOrNull?.param; lastAid = data.item?.lastOrNull?.param;
isLoadPrevious = null;
loadingState.value = Success(data.item); loadingState.value = Success(data.item);
return true; return true;
} }
@@ -116,13 +116,13 @@ class MemberVideoCtr
type: type, type: type,
mid: mid, mid: mid,
aid: type == ContributeType.video aid: type == ContributeType.video
? isLoadPrevious == true ? isLoadPrevious
? firstAid ? firstAid
: lastAid : lastAid
: null, : null,
order: type == ContributeType.video ? order.value : null, order: type == ContributeType.video ? order.value : null,
sort: type == ContributeType.video sort: type == ContributeType.video
? isLoadPrevious == true ? isLoadPrevious
? 'asc' ? 'asc'
: null : null
: sort.value, : sort.value,
@@ -130,12 +130,12 @@ class MemberVideoCtr
next: next, next: next,
seasonId: seasonId, seasonId: seasonId,
seriesId: seriesId, seriesId: seriesId,
includeCursor: isLocating.value == true && page == 0 ? true : null, includeCursor: isLocating.value && page == 0,
); );
void queryBySort() { void queryBySort() {
if (type == ContributeType.video) { if (type == ContributeType.video) {
isLocating.value = null; isLocating.value = false;
order.value = order.value == 'pubdate' ? 'click' : 'pubdate'; order.value = order.value == 'pubdate' ? 'click' : 'pubdate';
} else { } else {
sort.value = sort.value == 'desc' ? 'asc' : 'desc'; sort.value = sort.value == 'desc' ? 'asc' : 'desc';
@@ -223,7 +223,7 @@ class MemberVideoCtr
@override @override
Future<void> onReload() { Future<void> onReload() {
reload = true; reload = true;
isLocating.value = null; isLocating.value = false;
return super.onReload(); return super.onReload();
} }
} }

View File

@@ -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/pages/member_video/widgets/video_card_h_member_video.dart';
import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class MemberVideo extends StatefulWidget { class MemberVideo extends StatefulWidget {
@@ -59,8 +60,25 @@ class _MemberVideoState extends State<MemberVideo>
super.build(context); super.build(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final padding = MediaQuery.viewPaddingOf(context); final padding = MediaQuery.viewPaddingOf(context);
Widget child = refreshIndicator( final child = refreshIndicator(
onRefresh: _controller.onRefresh, 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( child: CustomScrollView(
physics: ReloadScrollPhysics(controller: _controller), physics: ReloadScrollPhysics(controller: _controller),
slivers: [ slivers: [
@@ -80,23 +98,21 @@ class _MemberVideoState extends State<MemberVideo>
children: [ children: [
child, child,
Obx( Obx(
() => _controller.isLocating.value != true () => !_controller.isLocating.value
? Positioned( ? Positioned(
right: 15 + padding.right, right: 15 + padding.right,
bottom: 15 + padding.bottom, bottom: 15 + padding.bottom,
child: FloatingActionButton.extended( child: FloatingActionButton.extended(
onPressed: () { onPressed: () {
final fromViewAid = _controller.fromViewAid; final fromViewAid = _controller.fromViewAid;
_controller _controller.isLocating.value = true;
..isLocating.value = true final locatedIndex =
..lastAid = fromViewAid; _controller.loadingState.value.dataOrNull
final locatedIndex = _controller ?.indexWhere((i) => i.param == fromViewAid) ??
.loadingState -1;
.value if (locatedIndex == -1) {
.dataOrNull
?.indexWhere((i) => i.param == fromViewAid);
if (locatedIndex == null || locatedIndex == -1) {
_controller _controller
..lastAid = fromViewAid
..reload = true ..reload = true
..page = 0 ..page = 0
..loadingState.value = LoadingState.loading() ..loadingState.value = LoadingState.loading()

View File

@@ -20,6 +20,9 @@ class MusicDetailController extends CommonDynController {
bool get showDynActionBar => Pref.showDynActionBar; bool get showDynActionBar => Pref.showDynActionBar;
String get shareUrl =>
'https://music.bilibili.com/h5/music-detail?music_id=${musicId}';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();

View File

@@ -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:PiliPlus/pages/common/common_list_controller.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
typedef MusicRecommendArgs = ({String id, MusicDetail item});
class MusicRecommendController class MusicRecommendController
extends CommonListController<List<BgmRecommend>?, BgmRecommend> { extends CommonListController<List<BgmRecommend>?, BgmRecommend> {
late final String musicId; late final String musicId;
@@ -13,9 +15,9 @@ class MusicRecommendController
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
final Map<String, dynamic> args = Get.arguments; final MusicRecommendArgs args = Get.arguments;
musicId = args['id']; musicId = args.id;
musicDetail = args['detail']; musicDetail = args.item;
queryData(); queryData();
} }

View File

@@ -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/models_new/music/bgm_recommend_list.dart';
import 'package:PiliPlus/pages/music/video/controller.dart'; import 'package:PiliPlus/pages/music/video/controller.dart';
import 'package:PiliPlus/pages/music/widget/music_video_card_h.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/grid.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -20,9 +20,9 @@ class MusicRecommandPage extends StatefulWidget {
class _MusicRecommandPageState extends State<MusicRecommandPage> class _MusicRecommandPageState extends State<MusicRecommandPage>
with GridMixin { with GridMixin {
late final _controller = Get.put( late final MusicRecommendController _controller = Get.putOrFind(
MusicRecommendController(), MusicRecommendController.new,
tag: Utils.generateRandomString(8), tag: (Get.arguments as MusicRecommendArgs).id,
); );
@override @override
@@ -34,7 +34,6 @@ class _MusicRecommandPageState extends State<MusicRecommandPage>
child: refreshIndicator( child: refreshIndicator(
onRefresh: _controller.onRefresh, onRefresh: _controller.onRefresh,
child: CustomScrollView( child: CustomScrollView(
controller: _controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
_buildAppBar(theme, padding), _buildAppBar(theme, padding),

View File

@@ -36,9 +36,9 @@ class MusicDetailPage extends StatefulWidget {
class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> { class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
@override @override
final MusicDetailController controller = Get.put( late final MusicDetailController controller = Get.putOrFind(
MusicDetailController(), MusicDetailController.new,
tag: Utils.generateRandomString(8), tag: Get.parameters['musicId']!,
); );
@override @override
@@ -110,7 +110,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
child = Padding( child = Padding(
padding: EdgeInsets.symmetric(horizontal: padding), padding: EdgeInsets.symmetric(horizontal: padding),
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
@@ -136,7 +136,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
Expanded( Expanded(
flex: flex, flex: flex,
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverPadding( SliverPadding(
@@ -235,7 +235,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
right: 0, right: 0,
bottom: 0, bottom: 0,
child: SlideTransition( child: SlideTransition(
position: controller.fabAnim, position: fabAnim,
child: controller.showDynActionBar child: controller.showDynActionBar
? Column( ? Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -288,9 +288,8 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
child: textIconButton( child: textIconButton(
icon: CustomIcon.share_node, icon: CustomIcon.share_node,
text: '分享', text: '分享',
onPressed: () => Utils.shareText( onPressed: () =>
'https://music.bilibili.com/h5/music-detail?music_id=${controller.musicId}', Utils.shareText(controller.shareUrl),
),
), ),
), ),
Expanded( Expanded(
@@ -565,7 +564,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
theme, theme,
() => Get.to( () => Get.to(
const MusicRecommandPage(), const MusicRecommandPage(),
arguments: {'id': controller.musicId, 'detail': item}, arguments: (id: controller.musicId, item: item),
), ),
), ),
], ],

View File

@@ -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/common/video/video_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.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/pgc/controller.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
@@ -74,8 +75,10 @@ class _SavePanelState extends State<SavePanel> {
//reply //reply
String? cover; String? cover;
_CoverType coverType = _CoverType.def16_9;
String? title; String? title;
int? pubdate; int? pubdate;
DateFormat dateFormat = DateUtil.longFormatDs;
String? uname; String? uname;
String uri = ''; String uri = '';
@@ -159,14 +162,42 @@ class _SavePanelState extends State<SavePanel> {
} else if (currentRoute.startsWith('/articlePage')) { } else if (currentRoute.startsWith('/articlePage')) {
try { try {
final type = reply.type.toInt(); final type = reply.type.toInt();
late final oid = reply.oid; final oid = reply.oid;
late final rootId = hasRoot ? reply.root : reply.id; final rootId = hasRoot ? reply.root : reply.id;
late final anchor = hasRoot ? 'anchor=${reply.id}&' : ''; final anchor = hasRoot ? 'anchor=${reply.id}&' : '';
late final enterUri = final enterUri =
'bilibili://following/detail/${Get.parameters['id'] ?? Get.arguments?['id']}'; 'bilibili://following/detail/${Get.parameters['id'] ?? Get.arguments?['id']}';
uri = uri =
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri'; 'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri';
} catch (_) {} } 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<MusicDetailController>(
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); if (kDebugMode) debugPrint(uri);
@@ -296,6 +327,7 @@ class _SavePanelState extends State<SavePanel> {
final theme = Theme.of(context); final theme = Theme.of(context);
final padding = MediaQuery.viewPaddingOf(context); final padding = MediaQuery.viewPaddingOf(context);
final maxWidth = context.mediaQueryShortestSide; final maxWidth = context.mediaQueryShortestSide;
late final coverSize = MediaQuery.textScalerOf(context).scale(65);
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: Get.back, onTap: Get.back,
@@ -367,15 +399,10 @@ class _SavePanelState extends State<SavePanel> {
NetworkImgLayer( NetworkImgLayer(
radius: 6, radius: 6,
src: cover!, src: cover!,
height: MediaQuery.textScalerOf( height: coverSize,
context, width: coverType == _CoverType.def16_9
).scale(65), ? coverSize * 16 / 9
width: : coverSize,
MediaQuery.textScalerOf(
context,
).scale(65) *
16 /
9,
quality: 100, quality: 100,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -394,7 +421,7 @@ class _SavePanelState extends State<SavePanel> {
Text( Text(
DateUtil.format( DateUtil.format(
pubdate, pubdate,
format: DateUtil.longFormatDs, format: dateFormat,
), ),
style: TextStyle( style: TextStyle(
color: theme.colorScheme.outline, color: theme.colorScheme.outline,
@@ -577,3 +604,5 @@ class _SavePanelState extends State<SavePanel> {
); );
} }
} }
enum _CoverType { def16_9, square }

View File

@@ -4,7 +4,6 @@ import 'dart:math' show pi, max;
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart' import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
show ImageModel; show ImageModel;
import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; 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/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/grpc/reply.dart'; import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/fav.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/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/cache_manage.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/feed_back.dart';
import 'package:PiliPlus/utils/image_util.dart'; import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
@@ -799,7 +797,7 @@ List<SettingsModel> get extraSettings => [
final res = await FavHttp.allFavFolders(Accounts.main.mid); final res = await FavHttp.allFavFolders(Accounts.main.mid);
if (res.isSuccess) { if (res.isSuccess) {
final list = res.data.list; final list = res.data.list;
if (list.isNullOrEmpty) { if (list == null || list.isEmpty) {
return; return;
} }
final quickFavId = Pref.quickFavId; final quickFavId = Pref.quickFavId;
@@ -809,22 +807,22 @@ List<SettingsModel> get extraSettings => [
title: const Text('选择默认收藏夹'), title: const Text('选择默认收藏夹'),
contentPadding: const EdgeInsets.only(top: 5, bottom: 18), contentPadding: const EdgeInsets.only(top: 5, bottom: 18),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Builder( child: RadioGroup(
builder: (context) => Column( onChanged: (value) {
children: List.generate(list!.length, (index) { Get.back();
final item = list[index]; GStorage.setting.put(SettingBoxKey.quickFavId, value);
return RadioWidget( SmartDialog.showToast('设置成功');
padding: const EdgeInsets.only(left: 14), },
title: item.title, groupValue: quickFavId,
groupValue: quickFavId, child: Column(
children: list.map((item) {
return RadioListTile(
toggleable: true,
dense: true,
title: Text(item.title),
value: item.id, value: item.id,
onChanged: (value) {
Get.back();
GStorage.setting.put(SettingBoxKey.quickFavId, value);
SmartDialog.showToast('设置成功');
},
); );
}), }).toList(),
), ),
), ),
), ),

View File

@@ -577,7 +577,7 @@ List<SettingsModel> get styleSettings => [
leading: const Icon(Icons.color_lens_outlined), leading: const Icon(Icons.color_lens_outlined),
title: '应用主题', title: '应用主题',
getSubtitle: () => getSubtitle: () =>
'当前主题:${Get.put(ColorSelectController()).type.value == 0 ? '动态取色' : '指定颜色'}', '当前主题:${Get.put(ColorSelectController()).dynamicColor.value ? '动态取色' : '指定颜色'}',
), ),
SettingsModel( SettingsModel(
settingsType: SettingsType.normal, settingsType: SettingsType.normal,

View File

@@ -98,13 +98,13 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
), ),
Obx( Obx(
() => ListTile( () => ListTile(
enabled: ctr.type.value != 0, enabled: !ctr.dynamicColor.value,
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text('调色板风格'), const Text('调色板风格'),
PopupMenuButton( PopupMenuButton(
enabled: ctr.type.value != 0, enabled: !ctr.dynamicColor.value,
initialValue: _dynamicSchemeVariant, initialValue: _dynamicSchemeVariant,
onSelected: (item) { onSelected: (item) {
_dynamicSchemeVariant = item; _dynamicSchemeVariant = item;
@@ -130,7 +130,7 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
style: TextStyle( style: TextStyle(
height: 1, height: 1,
fontSize: 13, fontSize: 13,
color: ctr.type.value == 0 color: ctr.dynamicColor.value
? theme.colorScheme.outline.withValues( ? theme.colorScheme.outline.withValues(
alpha: 0.8, alpha: 0.8,
) )
@@ -141,7 +141,7 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
Icon( Icon(
size: 20, size: 20,
Icons.keyboard_arrow_right, Icons.keyboard_arrow_right,
color: ctr.type.value == 0 color: ctr.dynamicColor.value
? theme.colorScheme.outline.withValues( ? theme.colorScheme.outline.withValues(
alpha: 0.8, alpha: 0.8,
) )
@@ -164,27 +164,14 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
), ),
), ),
Obx( Obx(
() => RadioListTile( () => CheckboxListTile(
value: 0,
title: const Text('动态取色'), title: const Text('动态取色'),
groupValue: ctr.type.value, controlAffinity: ListTileControlAffinity.leading,
onChanged: (dynamic val) { value: ctr.dynamicColor.value,
onChanged: (val) {
ctr ctr
..type.value = 0 ..dynamicColor.value = val!
..setting.put(SettingBoxKey.dynamicColor, true); ..setting.put(SettingBoxKey.dynamicColor, val);
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);
Get.forceAppUpdate(); Get.forceAppUpdate();
}, },
), ),
@@ -196,78 +183,79 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: Obx( child: Obx(
() => SizedBox( () => ctr.dynamicColor.value
height: ctr.type.value == 0 ? 0 : null, ? const SizedBox.shrink(key: ValueKey(false))
child: Padding( : Padding(
padding: const EdgeInsets.all(12), key: const ValueKey(true),
child: Wrap( padding: const EdgeInsets.all(12),
alignment: WrapAlignment.center, child: Wrap(
spacing: 22, alignment: WrapAlignment.center,
runSpacing: 18, spacing: 22,
children: colorThemeTypes.indexed.map( runSpacing: 18,
(e) { children: colorThemeTypes.indexed.map(
final index = e.$1; (e) {
final item = e.$2; final index = e.$1;
return GestureDetector( final item = e.$2;
behavior: HitTestBehavior.opaque, return GestureDetector(
onTap: () { behavior: HitTestBehavior.opaque,
ctr onTap: () {
..currentColor.value = index ctr
..setting.put(SettingBoxKey.customColor, index); ..currentColor.value = index
Get.forceAppUpdate(); ..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( ).toList(),
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(),
),
),
),
), ),
), ),
), ),
...[ Padding(
Padding( padding: padding,
padding: padding, child: IgnorePointer(
child: IgnorePointer( child: Container(
child: Container( height: size.height / 2,
height: size.height / 2, width: size.width,
width: size.width, color: theme.colorScheme.surface,
color: theme.colorScheme.surface, child: const HomePage(),
child: const HomePage(),
),
), ),
), ),
IgnorePointer( ),
child: NavigationBar( IgnorePointer(
destinations: NavigationBarType.values child: NavigationBar(
.map( destinations: NavigationBarType.values
(item) => NavigationDestination( .map(
icon: item.icon, (item) => NavigationDestination(
label: item.label, icon: item.icon,
), label: item.label,
) ),
.toList(), )
), .toList(),
), ),
], ),
], ],
), ),
); );
@@ -276,7 +264,6 @@ class _ColorSelectPageState extends State<ColorSelectPage> {
class ColorSelectController extends GetxController { class ColorSelectController extends GetxController {
final RxBool dynamicColor = Pref.dynamicColor.obs; final RxBool dynamicColor = Pref.dynamicColor.obs;
late final RxInt type = (dynamicColor.value ? 0 : 1).obs;
final RxInt currentColor = Pref.customColor.obs; final RxInt currentColor = Pref.customColor.obs;
final RxDouble currentTextScale = Pref.defaultTextScale.obs; final RxDouble currentTextScale = Pref.defaultTextScale.obs;
final Rx<ThemeType> themeType = Pref.themeType.obs; final Rx<ThemeType> themeType = Pref.themeType.obs;

View File

@@ -75,28 +75,30 @@ class _SetDisplayModeState extends State<SetDisplayMode> {
), ),
), ),
Expanded( Expanded(
child: ListView.builder( child: RadioGroup(
itemCount: modes.length, onChanged: (DisplayMode? newMode) {
itemBuilder: (context, index) { FlutterDisplayMode.setPreferredMode(
final DisplayMode mode = modes[index]; newMode!,
return RadioListTile<DisplayMode>( ).whenComplete(
value: mode, () => Future.delayed(
title: mode == DisplayMode.auto const Duration(milliseconds: 100),
? const Text('自动') fetchAll,
: Text('$mode${mode == active ? ' [系统]' : ''}'), ),
groupValue: preferred,
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<DisplayMode>(
value: mode,
title: mode == DisplayMode.auto
? const Text('自动')
: Text('$mode${mode == active ? ' [系统]' : ''}'),
);
},
),
), ),
), ),
], ],

View File

@@ -40,12 +40,12 @@ class _SlideColorPickerState extends State<SlideColorPicker> {
super.dispose(); super.dispose();
} }
String get _convert => Color.fromARGB( String get _convert => Color.fromRGBO(
255,
_r, _r,
_g, _g,
_b, _b,
).value.toRadixString(16).substring(2).toUpperCase(); 1,
).toARGB32().toRadixString(16).substring(2).toUpperCase();
Widget _slider({ Widget _slider({
required String title, required String title,

View File

@@ -33,24 +33,26 @@ class SelectDialog<T> extends StatelessWidget {
title: Text(title), title: Text(title),
contentPadding: const EdgeInsets.symmetric(vertical: 12), contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: RadioGroup<T>(
mainAxisSize: MainAxisSize.min, onChanged: Navigator.of(context).pop,
children: List.generate( groupValue: value,
values.length, child: Column(
(index) { mainAxisSize: MainAxisSize.min,
final item = values[index]; children: List.generate(
return RadioListTile<T>( values.length,
dense: true, (index) {
value: item.$1, final item = values[index];
title: Text( return RadioListTile<T>(
item.$2, dense: true,
style: titleMedium, value: item.$1,
), title: Text(
subtitle: subtitleBuilder?.call(context, index), item.$2,
groupValue: value, style: titleMedium,
onChanged: Navigator.of(context).pop, ),
); subtitle: subtitleBuilder?.call(context, index),
}, );
},
),
), ),
), ),
), ),

View File

@@ -423,7 +423,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
setting.put( setting.put(
SettingBoxKey.blockColor, SettingBoxKey.blockColor,
_blockColor _blockColor
.map((item) => item.value.toRadixString(16).substring(2)) .map((item) => item.toARGB32().toRadixString(16).substring(2))
.toList(), .toList(),
); );
(context as Element).markNeedsBuild(); (context as Element).markNeedsBuild();

View File

@@ -425,7 +425,7 @@ class VideoDetailController extends GetxController
bool get showVideoSheet => !horizontalScreen && !isPortrait; bool get showVideoSheet => !horizontalScreen && !isPortrait;
int? _lastPos; int? _lastPos;
List<PostSegmentModel>? postList; List<PostSegmentModel> postList = [];
RxList<SegmentModel> segmentList = <SegmentModel>[].obs; RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
List<Segment> viewPointList = <Segment>[]; List<Segment> viewPointList = <Segment>[];
List<Segment>? segmentProgressList; List<Segment>? segmentProgressList;
@@ -1316,9 +1316,8 @@ class VideoDetailController extends GetxController
} }
void onBlock(BuildContext context) { void onBlock(BuildContext context) {
postList ??= <PostSegmentModel>[]; if (postList.isEmpty) {
if (postList!.isEmpty) { postList.add(
postList!.add(
PostSegmentModel( PostSegmentModel(
segment: Pair( segment: Pair(
first: 0, first: 0,

View File

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class PopupMenuText<T> extends StatefulWidget {
final String title;
final T initialValue;
final PopupMenuItemSelected<T> onSelected;
final PopupMenuItemBuilder<T> 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<PopupMenuText<T>> createState() => _PopupMenuTextState();
}
class _PopupMenuTextState<T> extends State<PopupMenuText<T>> {
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<T>(
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,
),
],
),
),
],
);
}
}

View File

@@ -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/models_new/sponsor_block/segment_item.dart';
import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart'; import 'package:PiliPlus/pages/common/slide/common_collapse_slide_page.dart';
import 'package:PiliPlus/pages/video/controller.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/plugin/pl_player/controller.dart';
import 'package:PiliPlus/utils/duration_util.dart'; import 'package:PiliPlus/utils/duration_util.dart';
import 'package:PiliPlus/utils/extension.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/services.dart' show FilteringTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart' hide Response; import 'package:get/get.dart' hide Response;
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class PostPanel extends CommonCollapseSlidePage { class PostPanel extends CommonCollapseSlidePage {
const PostPanel({ const PostPanel({
@@ -60,130 +60,122 @@ class PostPanel extends CommonCollapseSlidePage {
required double currentPos, required double currentPos,
required double videoDuration, required double videoDuration,
}) { }) {
List<Widget> segment(BuildContext context, bool isFirst) { Widget segment(bool isFirst) => Builder(
String value = DurationUtil.formatDuration( builder: (context) {
isFirst ? item.segment.first : item.segment.second, String value = DurationUtil.formatDuration(
); isFirst ? item.segment.first : item.segment.second,
return [ );
Text( return Row(
'${isFirst ? '开始' : '结束'}: $value', spacing: 5,
), mainAxisSize: MainAxisSize.min,
iconButton( children: [
context: context, Text(
size: 26, '${isFirst ? '开始' : '结束'}: $value',
tooltip: '设为当前', ),
icon: Icons.my_location, iconButton(
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<String>(
context: context, context: context,
builder: (context) { size: 26,
String initV = value; tooltip: '设为当前',
return AlertDialog( icon: Icons.my_location,
content: TextFormField( onPressed: () {
initialValue: value, updateSegment(
autofocus: true, isFirst: isFirst,
onChanged: (value) => initV = value, item: item,
inputFormatters: [ value: currentPos,
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();
}, },
).then((res) { ),
if (res != null) { iconButton(
try { context: context,
List<num> split = res size: 26,
.split(':') tooltip: isFirst ? '视频开头' : '视频结尾',
.reversed icon: isFirst ? Icons.first_page : Icons.last_page,
.map(num.parse) onPressed: () {
.toList(); updateSegment(
double duration = 0; isFirst: isFirst,
for (int i = 0; i < split.length; i++) { item: item,
duration += split[i] * pow(60, i); value: isFirst ? 0 : videoDuration,
} );
if (duration <= videoDuration) { (context as Element).markNeedsBuild();
updateSegment( },
isFirst: isFirst, ),
item: item, iconButton(
value: duration, context: context,
size: 26,
tooltip: '编辑',
icon: Icons.edit,
onPressed: () async {
final res = await showDialog<String>(
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( if (res != null) {
builder: (context) => Row( try {
spacing: 5, List<num> split = res
mainAxisSize: MainAxisSize.min, .split(':')
children: segment(context, true), .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) { if (item.category != SegmentType.poi_highlight) {
return Wrap( return Wrap(
runSpacing: 8, runSpacing: 8,
spacing: 16, spacing: 16,
children: [ children: [segment(true), segment(false)],
child,
Builder(
builder: (context) => Row(
spacing: 5,
mainAxisSize: MainAxisSize.min,
children: segment(context, false),
),
),
],
); );
} }
return child; return segment(true);
} }
} }
@@ -191,7 +183,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
late final VideoDetailController videoDetailController = late final VideoDetailController videoDetailController =
widget.videoDetailController; widget.videoDetailController;
late final PlPlayerController plPlayerController = widget.plPlayerController; late final PlPlayerController plPlayerController = widget.plPlayerController;
late final List<PostSegmentModel>? list = videoDetailController.postList; late final List<PostSegmentModel> list = videoDetailController.postList;
late final double videoDuration = late final double videoDuration =
plPlayerController.durationSeconds.value.inMilliseconds / 1000; plPlayerController.durationSeconds.value.inMilliseconds / 1000;
@@ -224,7 +216,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
tooltip: '添加片段', tooltip: '添加片段',
onPressed: () { onPressed: () {
setState(() { setState(() {
list?.insert( list.insert(
0, 0,
PostSegmentModel( PostSegmentModel(
segment: Pair( segment: Pair(
@@ -263,18 +255,14 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
SingleChildScrollView( ListView.builder(
controller: _controller, controller: _controller,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(bottom: 88 + bottom), padding: EdgeInsets.only(bottom: 88 + bottom),
child: Column( itemCount: list.length,
children: List.generate( itemBuilder: (context, index) {
list!.length, return _buildItem(theme, index, list[index]);
(index) { },
return _buildItem(theme, index, list![index]);
},
),
),
), ),
Positioned( Positioned(
right: 16, right: 16,
@@ -294,10 +282,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
), ),
), ),
TextButton( TextButton(
onPressed: () { onPressed: _onPost,
Get.back();
_onPost();
},
child: const Text('确定提交'), child: const Text('确定提交'),
), ),
], ],
@@ -310,59 +295,56 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
); );
} }
void _onPost() { Future<void> _onPost() async {
Request() Get.back();
.post( final res = await Request().post(
'${widget.videoDetailController.blockServer}/api/skipSegments', '${widget.videoDetailController.blockServer}/api/skipSegments',
data: { data: {
'videoID': videoDetailController.bvid, 'videoID': videoDetailController.bvid,
'cid': videoDetailController.cid.value.toString(), 'cid': videoDetailController.cid.value.toString(),
'userID': Pref.blockUserID.toString(), 'userID': Pref.blockUserID.toString(),
'userAgent': Constants.userAgent, 'userAgent': Constants.userAgent,
'videoDuration': videoDuration, 'videoDuration': videoDuration,
'segments': list! 'segments': list
.map( .map(
(item) => { (item) => {
'segment': [ 'segment': [
item.segment.first, item.segment.first,
item.segment.second, item.segment.second,
], ],
'category': item.category.name, 'category': item.category.name,
'actionType': item.actionType.name, 'actionType': item.actionType.name,
}, },
) )
.toList(), .toList(),
}, },
options: Options( options: Options(
followRedirects: true, // Defaults to true. followRedirects: true, // Defaults to true.
validateStatus: (int? status) { validateStatus: (int? status) {
return (status! >= 200 && status < 300) || return (status! >= 200 && status < 300) ||
const [400, 403, 429, 409] // reduce extra toast const [400, 403, 429, 409] // reduce extra toast
.contains(status); .contains(status);
}, },
), ),
) );
.then(
(res) { if (res.statusCode == 200) {
if (res.statusCode == 200) { Get.back();
Get.back(); SmartDialog.showToast('提交成功');
SmartDialog.showToast('提交成功'); list.clear();
list?.clear(); if (res.data case List list) {
if (res.data case List list) { videoDetailController.handleSBData(
videoDetailController.handleSBData( list.map((e) => SegmentItemModel.fromJson(e)).toList(),
list.map((e) => SegmentItemModel.fromJson(e)).toList(),
);
}
plPlayerController.segmentList.value =
videoDetailController.segmentProgressList ?? <Segment>[];
if (videoDetailController.positionSubscription == null) {
videoDetailController.initSkip();
}
} else {
SmartDialog.showToast('提交失败: ${_errMsg(res)}');
}
},
); );
}
plPlayerController.segmentList.value =
videoDetailController.segmentProgressList ?? <Segment>[];
if (videoDetailController.positionSubscription == null) {
videoDetailController.initSkip();
}
} else {
SmartDialog.showToast('提交失败: ${_errMsg(res)}');
}
} }
String _errMsg(Response res) { String _errMsg(Response res) {
@@ -381,216 +363,142 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
} }
Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) { Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) {
return Builder( return Stack(
builder: (context) { clipBehavior: Clip.none,
return Stack( children: [
clipBehavior: Clip.none, Container(
children: [ width: double.infinity,
Container( margin: const EdgeInsets.symmetric(
margin: const EdgeInsets.symmetric( horizontal: 16,
horizontal: 16, vertical: 5,
vertical: 5, ),
), padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(12), decoration: BoxDecoration(
decoration: BoxDecoration( color: theme.colorScheme.onInverseSurface,
color: theme.colorScheme.onInverseSurface, borderRadius: const BorderRadius.all(Radius.circular(12)),
borderRadius: const BorderRadius.all(Radius.circular(12)), ),
), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
if (item.actionType != ActionType.full)
PostPanel.segmentWidget(
theme,
item: item,
currentPos: currentPos,
videoDuration: videoDuration,
),
Wrap(
runSpacing: 8,
spacing: 16,
children: [ children: [
if (item.actionType != ActionType.full) PopupMenuText(
PostPanel.segmentWidget( title: '分类',
theme, initialValue: item.category,
item: item, onSelected: (e) {
currentPos: currentPos, item.category = e;
videoDuration: videoDuration, List<ActionType> constraintList = e.toActionType;
), if (!constraintList.contains(item.actionType)) {
Wrap( item.actionType = constraintList.first;
runSpacing: 8, }
spacing: 16, switch (e) {
children: [ case SegmentType.poi_highlight:
Row( PostPanel.updateSegment(
mainAxisSize: MainAxisSize.min, isFirst: false,
children: [ item: item,
const Text('分类: '), value: item.segment.first,
PopupMenuButton<SegmentType>( );
initialValue: item.category, break;
onSelected: (e) { case SegmentType.exclusive_access:
item.category = e; PostPanel.updateSegment(
List<ActionType> constraintList = e.toActionType; isFirst: true,
if (!constraintList.contains(item.actionType)) { item: item,
item.actionType = constraintList.first; value: 0,
} );
switch (e) { break;
case SegmentType.poi_highlight: default:
PostPanel.updateSegment( }
isFirst: false, },
item: item, itemBuilder: (context) => SegmentType.values
value: item.segment.first, .map(
); (e) => PopupMenuItem(value: e, child: Text(e.title)),
break; )
case SegmentType.exclusive_access: .toList(),
PostPanel.updateSegment( getSelectTitle: (category) => category.title,
isFirst: true, ),
item: item, PopupMenuText(
value: 0, title: '行为类别',
); initialValue: item.actionType,
break; onSelected: (e) {
default: item.actionType = e;
} if (e == ActionType.full) {
(context as Element).markNeedsBuild(); PostPanel.updateSegment(
}, isFirst: true,
itemBuilder: (context) => SegmentType.values item: item,
.map( value: 0,
(e) => PopupMenuItem<SegmentType>( );
value: e, }
child: Text(e.title), },
), itemBuilder: (context) => ActionType.values
) .map(
.toList(), (e) => PopupMenuItem(
child: Row( enabled: item.category.toActionType.contains(e),
mainAxisSize: MainAxisSize.min, value: e,
children: [ child: Text(e.title),
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,
),
],
),
), ),
], )
), .toList(),
Row( getSelectTitle: (i) => i.title,
mainAxisSize: MainAxisSize.min,
children: [
const Text('行为类别: '),
PopupMenuButton<ActionType>(
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<ActionType>(
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,
),
],
),
),
],
),
],
), ),
], ],
), ),
), ],
Positioned( ),
top: 0, ),
right: 4, Positioned(
child: iconButton( top: 0,
context: context, right: 4,
size: 26, child: iconButton(
tooltip: '移除', context: context,
icon: Icons.clear, size: 26,
onPressed: () { tooltip: '移除',
setState(() { icon: Icons.clear,
list!.removeAt(index); onPressed: () {
}); setState(() {
}, list.removeAt(index);
), });
), },
Positioned( ),
top: 0, ),
left: 4, Positioned(
child: iconButton( top: 0,
context: context, left: 4,
size: 26, child: iconButton(
tooltip: '预览', context: context,
icon: Icons.preview_outlined, size: 26,
onPressed: () async { tooltip: '预览',
if (widget.plPlayerController.videoPlayerController != null) { icon: Icons.preview_outlined,
int start = max( onPressed: () async {
0, final videoCtr = widget.plPlayerController.videoPlayerController;
(item.segment.first * 1000).round() - 2000, if (videoCtr != null) {
); final start = (item.segment.first * 1000).round();
await widget.plPlayerController.videoPlayerController!.seek( final seek = max(0, start - 2000);
Duration(milliseconds: start), await videoCtr.seek(Duration(milliseconds: seek));
); if (!videoCtr.state.playing) {
if (!widget await videoCtr.play();
.plPlayerController }
.videoPlayerController! final delay = start - seek;
.state if (delay > 0) {
.playing) { await Future.delayed(Duration(milliseconds: delay));
await widget.plPlayerController.videoPlayerController! }
.play(); videoCtr.seek(
} Duration(milliseconds: (item.segment.second * 1000).round()),
if (start != 0) { );
await Future.delayed(const Duration(seconds: 2)); }
} },
widget.plPlayerController.videoPlayerController!.seek( ),
Duration( ),
milliseconds: (item.segment.second * 1000).round(), ],
),
);
}
},
),
),
],
);
},
); );
} }
} }

View File

@@ -456,7 +456,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
msg: editController.text, msg: editController.text,
mode: _mode.value, mode: _mode.value,
fontsize: _fontsize.value, fontsize: _fontsize.value,
color: isColorful ? null : _color.value.value & 0xFFFFFF, color: isColorful ? null : _color.value.toARGB32() & 0xFFFFFF,
colorful: isColorful, colorful: isColorful,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();

View File

@@ -83,9 +83,7 @@ class HeaderControlState extends TripleState<HeaderControl> {
Timer? clock; Timer? clock;
bool get isFullScreen => plPlayerController.isFullScreen.value; bool get isFullScreen => plPlayerController.isFullScreen.value;
Box setting = GStorage.setting; Box setting = GStorage.setting;
MarqueeController? marqueeController; late final provider = ContextSingleTicker(context);
MarqueeController get _marqueeController =>
marqueeController ??= MarqueeController(autoStart: false);
@override @override
void initState() { void initState() {
@@ -100,8 +98,6 @@ class HeaderControlState extends TripleState<HeaderControl> {
@override @override
void dispose() { void dispose() {
clock?.cancel(); clock?.cancel();
marqueeController?.dispose();
marqueeController = null;
super.dispose(); super.dispose();
} }
@@ -1934,11 +1930,11 @@ class HeaderControlState extends TripleState<HeaderControl> {
title, title,
spacing: 30, spacing: 30,
velocity: 30, velocity: 30,
controller: _marqueeController,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 16, fontSize: 16,
), ),
provider: provider,
); );
}, },
), ),

View File

@@ -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/models_new/msg/session_ss/data.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -154,20 +153,9 @@ class WhisperLinkSettingController extends GetxController {
} }
} }
void report() { void report() => showMemberReportDialog(
showDialog( Get.context!,
context: Get.context!, name: userState.value.dataOrNull?.firstOrNull?.name,
builder: (context) => AlertDialog( mid: talkerUid,
clipBehavior: Clip.hardEdge, );
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
content: MemberReportPanel(
name: userState.value.dataOrNull?.firstOrNull?.name ?? '',
mid: talkerUid,
),
),
);
}
} }

View File

@@ -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/common/common_intro_controller.dart';
import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/introduction/pgc/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/pages/video/post_panel/view.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/plugin/pl_player/models/bottom_control_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_control_type.dart';
@@ -187,22 +188,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
super.initState(); super.initState();
_controlsListener = plPlayerController.showControls.listen((bool val) { _controlsListener = plPlayerController.showControls.listen((bool val) {
final visible = val && !plPlayerController.controlsLock.value; final visible = val && !plPlayerController.controlsLock.value;
widget.videoDetailController?.headerCtrKey.currentState?.provider.muted =
!visible;
if (visible) { if (visible) {
animationController.forward(); animationController.forward();
widget
.videoDetailController
?.headerCtrKey
.currentState
?.marqueeController
?.start();
} else { } else {
animationController.reverse(); animationController.reverse();
widget
.videoDetailController
?.headerCtrKey
.currentState
?.marqueeController
?.stop();
} }
}); });
animationController = AnimationController( animationController = AnimationController(
@@ -1876,9 +1867,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
); );
} }
late final segment = Pair(
first: plPlayerController.position.value.inMilliseconds / 1000.0,
second: plPlayerController.position.value.inMilliseconds / 1000.0,
);
Future<void> screenshotWebp() async { Future<void> screenshotWebp() async {
final videoCtr = widget.videoDetailController!; final videoCtr = widget.videoDetailController!;
final videoInfo = widget.videoDetailController!.data; final videoInfo = videoCtr.data;
final ids = videoInfo.dash!.video!.map((i) => i.id!).toSet(); final ids = videoInfo.dash!.video!.map((i) => i.id!).toSet();
final video = videoCtr.findVideoByQa(ids.reduce((p, n) => p < n ? p : n)); final video = videoCtr.findVideoByQa(ids.reduce((p, n) => p < n ? p : n));
@@ -1890,7 +1885,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
final theme = Theme.of(context); final theme = Theme.of(context);
final currentPos = ctr.position.value.inMilliseconds / 1000.0; final currentPos = ctr.position.value.inMilliseconds / 1000.0;
final duration = ctr.durationSeconds.value.inMilliseconds / 1000.0; final duration = ctr.durationSeconds.value.inMilliseconds / 1000.0;
final segment = Pair(first: currentPos, second: currentPos + 10.0);
final model = PostSegmentModel( final model = PostSegmentModel(
segment: segment, segment: segment,
category: SegmentType.sponsor, category: SegmentType.sponsor,
@@ -1915,43 +1909,37 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
currentPos: currentPos, currentPos: currentPos,
videoDuration: duration, videoDuration: duration,
), ),
Builder( PopupMenuText(
builder: (context) => PopupMenuButton( title: '选择画质',
initialValue: qa.code, initialValue: qa.code,
onSelected: (value) { onSelected: (value) {
if (value == qa.code) return; final video = videoCtr.findVideoByQa(value);
final video = videoCtr.findVideoByQa(value); url = video.baseUrl;
url = video.baseUrl; qa = video.quality;
qa = video.quality; },
(context as Element).markNeedsBuild(); itemBuilder: (context) => videoInfo.supportFormats!
}, .map(
itemBuilder: (_) => videoInfo.supportFormats! (i) => PopupMenuItem(
.map( enabled: ids.contains(i.quality),
(i) => PopupMenuItem<int>( value: i.quality,
enabled: ids.contains(i.quality), child: Text(i.newDesc ?? ''),
value: i.quality, ),
child: Text(i.newDesc ?? ''), )
), .toList(),
) getSelectTitle: (_) => qa.shortDesc,
.toList(),
child: Text('转码画质:${qa.shortDesc}'),
),
), ),
Builder( PopupMenuText(
builder: (context) => PopupMenuButton( title: 'webp预设',
initialValue: preset, initialValue: preset,
onSelected: (value) { onSelected: (value) {
if (preset == value) return; if (preset == value) return;
preset = value; preset = value;
(context as Element).markNeedsBuild(); (context as Element).markNeedsBuild();
}, },
itemBuilder: (_) => WebpPreset.values itemBuilder: (context) => WebpPreset.values
.map( .map((i) => PopupMenuItem(value: i, child: Text(i.name)))
(i) => PopupMenuItem(value: i, child: Text(i.name)), .toList(),
) getSelectTitle: (i) => '${i.name}(${i.desc})',
.toList(),
child: Text('webp预设${preset.name}${preset.desc}'),
),
), ),
Text( Text(
'*转码使用软解,速度可能慢于播放,请不要选择过长的时间段或过高画质', '*转码使用软解,速度可能慢于播放,请不要选择过长的时间段或过高画质',

View File

@@ -242,12 +242,12 @@ class PiliScheme {
case 'comment': case 'comment':
if (path.startsWith("/detail/")) { 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 // 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<String> pathSegments = uri.pathSegments; final pathSegments = uri.pathSegments;
Map<String, String> queryParameters = uri.queryParameters; final queryParameters = uri.queryParameters;
int type = int.parse(pathSegments[1]); // business_id final type = int.parse(pathSegments[1]); // business_id
int oid = int.parse(pathSegments[2]); // subject_id final oid = int.parse(pathSegments[2]); // subject_id
int rootId = int.parse(pathSegments[3]); // root_id // target_id final rootId = int.parse(pathSegments[3]); // root_id // target_id
int? rpId = final rpId =
queryParameters['anchor'] != queryParameters['anchor'] !=
null // source_id null // source_id
? int.tryParse(queryParameters['anchor']!) ? int.tryParse(queryParameters['anchor']!)
@@ -255,34 +255,39 @@ class PiliScheme {
// int subType = int.parse(queryParameters['subType'] ?? '0'); // int subType = int.parse(queryParameters['subType'] ?? '0');
// int extraIntentId = // int extraIntentId =
// int.parse(queryParameters['extraIntentId'] ?? '0'); // int.parse(queryParameters['extraIntentId'] ?? '0');
final enterUri = queryParameters['enterUri'];
Get.to( Get.to(
arguments: { arguments: {
'oid': oid, 'oid': oid,
'rpid': rootId, 'rpid': rootId,
'id': rpId, 'id': rpId,
'type': type, 'type': type,
'enterUri': queryParameters['enterUri'], 'enterUri': enterUri,
}, },
() => Scaffold( () => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
title: const Text('评论详情'), title: const Text('评论详情'),
actions: [ actions:
IconButton( enterUri != null || const [11, 16, 17].contains(type)
tooltip: '前往', ? [
onPressed: () { IconButton(
String? enterUri = queryParameters['enterUri']; tooltip: '前往',
if (enterUri != null) { onPressed: () {
routePush(Uri.parse(enterUri)); if (enterUri != null) {
} else { routePush(Uri.parse(enterUri));
routePush( } else {
Uri.parse('bilibili://following/detail/$oid'), routePush(
); Uri.parse(
} 'bilibili://following/detail/$oid',
}, ),
icon: const Icon(Icons.open_in_new), );
), }
], },
icon: const Icon(Icons.open_in_new),
),
]
: null,
), ),
body: ViewSafeArea( body: ViewSafeArea(
child: VideoReplyReplyPanel( child: VideoReplyReplyPanel(
@@ -729,9 +734,12 @@ class PiliScheme {
case 'bangumi': case 'bangumi':
// www.bilibili.com/bangumi/play/ep{eid}?start_progress={offset}&thumb_up_dm_id={dmid} // www.bilibili.com/bangumi/play/ep{eid}?start_progress={offset}&thumb_up_dm_id={dmid}
// if (kDebugMode) debugPrint('番剧'); // if (kDebugMode) debugPrint('番剧');
final queryParameters = uri.queryParameters;
bool hasMatch = PageUtils.viewPgcFromUri( bool hasMatch = PageUtils.viewPgcFromUri(
path, path,
progress: uri.queryParameters['start_progress'], progress:
queryParameters['start_progress'] ??
queryParameters['dm_progress'],
); );
if (hasMatch) { if (hasMatch) {
return true; return true;

View File

@@ -2,17 +2,12 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class CacheManage { abstract class CacheManage {
CacheManage._internal();
static final CacheManage cacheManage = CacheManage._internal();
factory CacheManage() => cacheManage;
// 获取缓存目录 // 获取缓存目录
Future<double> loadApplicationCache() async { static Future<double> loadApplicationCache() async {
/// clear all of image in memory /// clear all of image in memory
// clearMemoryImageCache(); // clearMemoryImageCache();
/// get ImageCache /// get ImageCache
@@ -47,7 +42,9 @@ class CacheManage {
} }
// 循环计算文件的大小(递归) // 循环计算文件的大小(递归)
Future<double> getTotalSizeOfFilesInDir(final FileSystemEntity file) async { static Future<double> getTotalSizeOfFilesInDir(
final FileSystemEntity file,
) async {
if (file is File) { if (file is File) {
int length = await file.length(); int length = await file.length();
return double.parse(length.toString()); return double.parse(length.toString());
@@ -76,7 +73,7 @@ class CacheManage {
} }
/// 清除 Documents 目录下的 DioCache.db /// 清除 Documents 目录下的 DioCache.db
Future<void> clearApplicationCache() async { static Future<void> clearApplicationCache() async {
Directory directory = await getApplicationDocumentsDirectory(); Directory directory = await getApplicationDocumentsDirectory();
if (directory.existsSync()) { if (directory.existsSync()) {
String dioCacheFileName = String dioCacheFileName =
@@ -103,7 +100,7 @@ class CacheManage {
} }
/// 递归方式删除目录及文件 /// 递归方式删除目录及文件
Future<void> deleteDirectory(FileSystemEntity file) async { static Future<void> deleteDirectory(FileSystemEntity file) async {
if (file is Directory) { if (file is Directory) {
final List<FileSystemEntity> children = file.listSync(); final List<FileSystemEntity> children = file.listSync();
for (final FileSystemEntity child in children) { for (final FileSystemEntity child in children) {
@@ -112,4 +109,18 @@ class CacheManage {
} }
await file.delete(); await file.delete();
} }
static Future<void> 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();
}
}
}
}
} }

View File

@@ -52,7 +52,7 @@ extension ContextExtensions on BuildContext {
double get devicePixelRatio => MediaQuery.devicePixelRatioOf(this); double get devicePixelRatio => MediaQuery.devicePixelRatioOf(this);
/// similar to [MediaQuery.of(this).textScaleFactor] /// similar to [MediaQuery.of(this).textScaleFactor]
double get textScaleFactor => MediaQuery.textScaleFactorOf(this); TextScaler get textScaler => MediaQuery.textScalerOf(this);
/// get the shortestSide from screen /// get the shortestSide from screen
double get mediaQueryShortestSide => mediaQuerySize.shortestSide; double get mediaQueryShortestSide => mediaQuerySize.shortestSide;

View File

@@ -223,3 +223,8 @@ extension FileExt on File {
extension SizeExt on Size { extension SizeExt on Size {
bool get isPortrait => width < 600 || height >= width; bool get isPortrait => width < 600 || height >= width;
} }
extension GetExt on GetInterface {
S putOrFind<S>(InstanceBuilderCallback<S> dep, {String? tag}) =>
GetInstance().putOrFind(dep, tag: tag);
}

View File

@@ -292,14 +292,14 @@ class PageUtils {
static Future<void> pushDynFromId({id, rid, bool off = false}) async { static Future<void> pushDynFromId({id, rid, bool off = false}) async {
SmartDialog.showLoading(); SmartDialog.showLoading();
var res = await DynamicsHttp.dynamicDetail( final res = await DynamicsHttp.dynamicDetail(
id: id, id: id,
rid: rid, rid: rid,
type: rid != null ? 2 : null, type: rid != null ? 2 : null,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
if (res['status']) { if (res.isSuccess) {
DynamicItemModel data = res['data']; final data = res.data;
if (data.basic?.commentType == 12) { if (data.basic?.commentType == 12) {
toDupNamed( toDupNamed(
'/articlePage', '/articlePage',
@@ -313,13 +313,13 @@ class PageUtils {
toDupNamed( toDupNamed(
'/dynamicDetail', '/dynamicDetail',
arguments: { arguments: {
'item': res['data'], 'item': data,
}, },
off: off, off: off,
); );
} }
} else { } else {
SmartDialog.showToast(res['msg']); res.toast();
} }
} }

View File

@@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; 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/im/type.pbenum.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo; show ReplyInfo;
@@ -288,17 +287,17 @@ class RequestUtils {
if (id != null) { if (id != null) {
await Future.delayed(const Duration(milliseconds: 200)); await Future.delayed(const Duration(milliseconds: 200));
var res = await DynamicsHttp.dynamicDetail(id: id); var res = await DynamicsHttp.dynamicDetail(id: id);
if (res['status']) { if (res.isSuccess) {
final ctr = Get.find<DynamicsTabController>(tag: 'all'); final ctr = Get.find<DynamicsTabController>(tag: 'all');
if (ctr.loadingState.value.isSuccess) { if (ctr.loadingState.value.isSuccess) {
List<DynamicItemModel>? list = ctr.loadingState.value.data; List<DynamicItemModel>? list = ctr.loadingState.value.data;
if (list != null) { if (list != null) {
list.insert(0, res['data']); list.insert(0, res.data);
ctr.loadingState.refresh(); ctr.loadingState.refresh();
return; return;
} }
} }
ctr.loadingState.value = Success([res['data']]); ctr.loadingState.value = Success([res.data]);
} }
} }
} catch (e) { } catch (e) {
@@ -318,7 +317,7 @@ class RequestUtils {
await Future.delayed(const Duration(seconds: 5)); await Future.delayed(const Duration(seconds: 5));
} }
var res = await DynamicsHttp.dynamicDetail(id: id, clearCookie: true); var res = await DynamicsHttp.dynamicDetail(id: id, clearCookie: true);
bool isBan = !res['status']; bool isBan = !res.isSuccess;
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: const Text('动态检查结果'), title: const Text('动态检查结果'),
@@ -400,23 +399,20 @@ class RequestUtils {
title: Text('${isCopy ? '复制' : '移动'}'), title: Text('${isCopy ? '复制' : '移动'}'),
contentPadding: const EdgeInsets.only(top: 5), contentPadding: const EdgeInsets.only(top: 5),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Builder( child: RadioGroup(
builder: (context) => Column( onChanged: (value) {
children: List.generate(list.length, (index) { checkedId = value;
final item = list[index]; (context as Element).markNeedsBuild();
return RadioWidget<int>( },
padding: const EdgeInsets.only(left: 14), groupValue: checkedId,
title: item.title, child: Column(
groupValue: checkedId, children: list.map((item) {
return RadioListTile<int>(
dense: true,
title: Text(item.title),
value: item.id, value: item.id,
onChanged: (value) {
checkedId = value;
if (context.mounted) {
(context as Element).markNeedsBuild();
}
},
); );
}), }).toList(),
), ),
), ),
), ),

View File

@@ -12,7 +12,7 @@ import 'package:PiliPlus/utils/set_int_adapter.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class GStorage { abstract class GStorage {
static late final Box<UserInfoData> userInfo; static late final Box<UserInfoData> userInfo;
static late final Box<dynamic> historyWord; static late final Box<dynamic> historyWord;
static late final Box<dynamic> localCache; static late final Box<dynamic> localCache;

View File

@@ -33,7 +33,7 @@ import 'package:get/get.dart' hide ContextExtensionss;
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class Pref { abstract class Pref {
static final Box _setting = GStorage.setting; static final Box _setting = GStorage.setting;
static final Box _video = GStorage.video; static final Box _video = GStorage.video;
static final Box _localCache = GStorage.localCache; static final Box _localCache = GStorage.localCache;