Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
196 lines
6.6 KiB
Dart
196 lines
6.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
class FancyScaffold extends StatefulWidget {
|
|
final Widget bottomPanel;
|
|
final double bottomPanelHeight;
|
|
final Widget expandedPanel;
|
|
final Widget? bottomNavigationBar;
|
|
final Widget? drawer;
|
|
final Widget? navigationRail;
|
|
final Widget body;
|
|
final void Function(AnimationStatus)? onAnimationStatusChange;
|
|
|
|
const FancyScaffold({
|
|
required this.bottomPanel,
|
|
required this.bottomPanelHeight,
|
|
required this.expandedPanel,
|
|
required this.body,
|
|
this.onAnimationStatusChange,
|
|
this.bottomNavigationBar,
|
|
this.navigationRail,
|
|
this.drawer,
|
|
super.key,
|
|
});
|
|
|
|
static FancyScaffoldState? of(BuildContext context) =>
|
|
context.findAncestorStateOfType<FancyScaffoldState>();
|
|
|
|
@override
|
|
FancyScaffoldState createState() => FancyScaffoldState();
|
|
}
|
|
|
|
class FancyScaffoldState extends State<FancyScaffold>
|
|
with TickerProviderStateMixin {
|
|
// goes from 0 to 1 (double)
|
|
// 0 = preview, 1 = expanded
|
|
late final AnimationController dragController;
|
|
final statusNotifier =
|
|
ValueNotifier<AnimationStatus>(AnimationStatus.dismissed);
|
|
|
|
@override
|
|
void initState() {
|
|
dragController = AnimationController(
|
|
vsync: this, duration: const Duration(milliseconds: 500));
|
|
dragController.addStatusListener((status) => statusNotifier.value = status);
|
|
statusNotifier.addListener(
|
|
() => widget.onAnimationStatusChange?.call(statusNotifier.value));
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
dragController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final systemPadding = MediaQuery.of(context).viewPadding;
|
|
final defaultBottomPadding =
|
|
(widget.bottomNavigationBar == null ? 0 : 80.0) + systemPadding.bottom;
|
|
final screenHeight = MediaQuery.of(context).size.height;
|
|
final sizeAnimation = Tween<double>(
|
|
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
|
|
end: 1.0,
|
|
).animate(dragController);
|
|
return WillPopScope(
|
|
onWillPop: () {
|
|
if (statusNotifier.value == AnimationStatus.completed ||
|
|
statusNotifier.value == AnimationStatus.reverse) {
|
|
dragController.fling(velocity: -1.0);
|
|
return Future.value(false);
|
|
}
|
|
|
|
return Future.value(true);
|
|
},
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Scaffold(
|
|
body: widget.navigationRail != null
|
|
? Row(children: [
|
|
widget.navigationRail!,
|
|
const VerticalDivider(
|
|
indent: 0.0,
|
|
endIndent: 0.0,
|
|
width: 2.0,
|
|
),
|
|
Expanded(child: widget.body)
|
|
])
|
|
: widget.body,
|
|
drawer: widget.drawer,
|
|
bottomNavigationBar: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(height: widget.bottomPanelHeight),
|
|
if (widget.bottomNavigationBar != null)
|
|
SizeTransition(
|
|
axisAlignment: -1.0,
|
|
sizeFactor:
|
|
Tween(begin: 1.0, end: 0.0).animate(sizeAnimation),
|
|
child: widget.bottomNavigationBar,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: AnimatedBuilder(
|
|
animation: sizeAnimation,
|
|
builder: (context, child) {
|
|
final x = 1.0 - sizeAnimation.value;
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: (defaultBottomPadding /*+ 8.0*/) * x,
|
|
//right: 8.0 * x,
|
|
//left: 8.0 * x,
|
|
),
|
|
child: child,
|
|
);
|
|
},
|
|
child: ValueListenableBuilder(
|
|
valueListenable: statusNotifier,
|
|
builder: (context, state, child) {
|
|
return GestureDetector(
|
|
onVerticalDragEnd: _onVerticalDragEnd,
|
|
onVerticalDragUpdate: _onVerticalDragUpdate,
|
|
child: child,
|
|
);
|
|
},
|
|
child: SizeTransition(
|
|
sizeFactor: sizeAnimation,
|
|
axisAlignment: -1.0,
|
|
axis: Axis.vertical,
|
|
child: SizedBox(
|
|
height: screenHeight,
|
|
width: MediaQuery.of(context).size.width,
|
|
child: ValueListenableBuilder(
|
|
valueListenable: statusNotifier,
|
|
builder: (context, state, _) => Stack(
|
|
children: [
|
|
if (state != AnimationStatus.dismissed)
|
|
Positioned.fill(
|
|
key: const Key('player_screen'),
|
|
child: widget.expandedPanel,
|
|
),
|
|
if (state != AnimationStatus.completed)
|
|
Positioned(
|
|
top: 0,
|
|
right: 0,
|
|
left: 0,
|
|
key: const Key('player_bar'),
|
|
child: FadeTransition(
|
|
opacity: Tween(begin: 1.0, end: 0.0)
|
|
.animate(dragController),
|
|
child: SizedBox(
|
|
height: widget.bottomPanelHeight,
|
|
child: widget.bottomPanel),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onVerticalDragUpdate(DragUpdateDetails details) {
|
|
dragController.value -=
|
|
details.delta.dy / MediaQuery.of(context).size.height;
|
|
}
|
|
|
|
void _onVerticalDragEnd(DragEndDetails details) {
|
|
// snap widget to size
|
|
// this should be also handled by drag velocity and not only with bare size.
|
|
|
|
const double minFlingVelocity = 365.0;
|
|
|
|
if (details.velocity.pixelsPerSecond.dy.abs() > minFlingVelocity) {
|
|
dragController.fling(
|
|
velocity: -details.velocity.pixelsPerSecond.dy /
|
|
MediaQuery.of(context).size.height);
|
|
return;
|
|
}
|
|
|
|
dragController.fling(velocity: dragController.value > 0.5 ? 1.0 : -1.0);
|
|
}
|
|
}
|