Podcast page transition with fixed player.

This commit is contained in:
stonega 2021-01-03 00:48:26 +08:00
parent daaeb7c8c1
commit 54268cf8b9
3 changed files with 171 additions and 53 deletions

View File

@ -10,7 +10,7 @@ import 'package:focused_menu/modals.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart' as tuple;
import '../episodes/episode_detail.dart'; import '../episodes/episode_detail.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
@ -26,9 +26,11 @@ import '../type/episodebrief.dart';
import '../type/play_histroy.dart'; import '../type/play_histroy.dart';
import '../type/podcastlocal.dart'; import '../type/podcastlocal.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import '../util/hide_player_route.dart';
import '../util/pageroute.dart'; import '../util/pageroute.dart';
import '../widgets/custom_widget.dart'; import '../widgets/custom_widget.dart';
import '../widgets/general_dialog.dart'; import '../widgets/general_dialog.dart';
import 'home.dart';
class ScrollPodcasts extends StatefulWidget { class ScrollPodcasts extends StatefulWidget {
@override @override
@ -106,8 +108,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
final s = context.s; final s = context.s;
return Selector<GroupList, Tuple2<List<PodcastGroup>, bool>>( return Selector<GroupList, tuple.Tuple2<List<PodcastGroup>, bool>>(
selector: (_, groupList) => Tuple2(groupList.groups, groupList.created), selector: (_, groupList) =>
tuple.Tuple2(groupList.groups, groupList.created),
builder: (_, data, __) { builder: (_, data, __) {
var groups = data.item1; var groups = data.item1;
var import = data.item2; var import = data.item2;
@ -441,12 +444,23 @@ class _ScrollPodcastsState extends State<ScrollPodcasts>
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
SlideLeftRoute( HidePlayerRoute(
page: PodcastDetail( PodcastDetail(
podcastLocal: podcastLocal, podcastLocal: podcastLocal,
)), ),
); PodcastDetail(
podcastLocal:
podcastLocal,
hide: true),
duration: Duration(
milliseconds: 300),
)
// SlideLeftRoute(
// page: PodcastDetail(
// podcastLocal: podcastLocal,
// )),
);
}, },
child: PodcastPreview( child: PodcastPreview(
podcastLocal: podcastLocal, podcastLocal: podcastLocal,
@ -553,7 +567,7 @@ class ShowEpisode extends StatelessWidget {
final DBHelper _dbHelper = DBHelper(); final DBHelper _dbHelper = DBHelper();
ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key); ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key);
Future<Tuple5<int, bool, bool, bool, List<int>>> _initData( Future<tuple.Tuple5<int, bool, bool, bool, List<int>>> _initData(
EpisodeBrief episode) async { EpisodeBrief episode) async {
final menuList = await _getEpisodeMenu(); final menuList = await _getEpisodeMenu();
final tapToOpen = await _getTapToOpenPopupMenu(); final tapToOpen = await _getTapToOpenPopupMenu();
@ -561,7 +575,7 @@ class ShowEpisode extends StatelessWidget {
final liked = await _isLiked(episode); final liked = await _isLiked(episode);
final downloaded = await _isDownloaded(episode); final downloaded = await _isDownloaded(episode);
return Tuple5(listened, liked, downloaded, tapToOpen, menuList); return tuple.Tuple5(listened, liked, downloaded, tapToOpen, menuList);
} }
Future<int> _isListened(EpisodeBrief episode) async { Future<int> _isListened(EpisodeBrief episode) async {
@ -690,17 +704,17 @@ class ShowEpisode extends StatelessWidget {
(context, index) { (context, index) {
final c = podcastLocal.backgroudColor(context); final c = podcastLocal.backgroudColor(context);
return Selector<AudioPlayerNotifier, return Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, List<String>>>( tuple.Tuple2<EpisodeBrief, List<String>>>(
selector: (_, audio) => Tuple2( selector: (_, audio) => tuple.Tuple2(
audio?.episode, audio?.episode,
audio.queue.episodes audio.queue.episodes
.map((e) => e.enclosureUrl) .map((e) => e.enclosureUrl)
.toList(), .toList(),
), ),
builder: (_, data, __) => FutureBuilder< builder: (_, data, __) => FutureBuilder<
Tuple5<int, bool, bool, bool, List<int>>>( tuple.Tuple5<int, bool, bool, bool, List<int>>>(
future: _initData(episodes[index]), future: _initData(episodes[index]),
initialData: Tuple5(0, false, false, false, []), initialData: tuple.Tuple5(0, false, false, false, []),
builder: (context, snapshot) { builder: (context, snapshot) {
final isListened = snapshot.data.item1; final isListened = snapshot.data.item1;
final isLiked = snapshot.data.item2; final isLiked = snapshot.data.item2;
@ -919,11 +933,13 @@ class ShowEpisode extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
Selector<AudioPlayerNotifier, Selector<
Tuple2<EpisodeBrief, bool>>( AudioPlayerNotifier,
selector: (_, audio) => Tuple2( tuple.Tuple2<EpisodeBrief,
audio.episode, bool>>(
audio.playerRunning), selector: (_, audio) =>
tuple.Tuple2(audio.episode,
audio.playerRunning),
builder: (_, data, __) { builder: (_, data, __) {
return (episodes[index] return (episodes[index]
.enclosureUrl == .enclosureUrl ==

View File

@ -30,7 +30,7 @@ import '../widgets/general_dialog.dart';
import '../widgets/muiliselect_bar.dart'; import '../widgets/muiliselect_bar.dart';
import 'podcast_settings.dart'; import 'podcast_settings.dart';
const KDefaultAvatar = """http://xuanmei.us/assets/default/avatar_small- const String kDefaultAvatar = """http://xuanmei.us/assets/default/avatar_small-
170afdc2be97fc6148b283083942d82c101d4c1061f6b28f87c8958b52664af9.jpg"""; 170afdc2be97fc6148b283083942d82c101d4c1061f6b28f87c8958b52664af9.jpg""";
class PodcastDetail extends StatefulWidget { class PodcastDetail extends StatefulWidget {
@ -89,7 +89,6 @@ class _PodcastDetailState extends State<PodcastDetail> {
bool _selectAll; bool _selectAll;
bool _selectBefore; bool _selectBefore;
bool _selectAfter; bool _selectAfter;
bool _loadEpisodes = false;
///Show podcast info. ///Show podcast info.
bool _showInfo; bool _showInfo;
@ -106,8 +105,6 @@ class _PodcastDetailState extends State<PodcastDetail> {
_selectAfter = false; _selectAfter = false;
_selectBefore = false; _selectBefore = false;
_showInfo = false; _showInfo = false;
Future.delayed(Duration(milliseconds: 200))
.then((value) => setState(() => _loadEpisodes = true));
} }
@override @override
@ -293,8 +290,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
if (snapshot.hasData) if (snapshot.hasData)
...snapshot.data.item2 ...snapshot.data.item2
.map<Widget>((host) { .map<Widget>((host) {
final image = host.image == KDefaultAvatar final image = host.image == kDefaultAvatar
? KDefaultAvatar ? kDefaultAvatar
: host.image; : host.image;
return Container( return Container(
padding: EdgeInsets.fromLTRB(5, 10, 5, 0), padding: EdgeInsets.fromLTRB(5, 10, 5, 0),
@ -615,35 +612,36 @@ class _PodcastDetailState extends State<PodcastDetail> {
} }
}), }),
Spacer(), Spacer(),
Material( if (!widget.hide)
color: Colors.transparent, Material(
clipBehavior: Clip.hardEdge, color: Colors.transparent,
borderRadius: BorderRadius.circular(100), clipBehavior: Clip.hardEdge,
child: TweenAnimationBuilder( borderRadius: BorderRadius.circular(100),
duration: Duration(milliseconds: 500), child: TweenAnimationBuilder(
curve: Curves.easeInOutQuart, duration: Duration(milliseconds: 500),
tween: Tween<double>(begin: 0.0, end: 1.0), curve: Curves.easeInOutQuart,
builder: (context, angle, child) => Transform.rotate( tween: Tween<double>(begin: 0.0, end: 1.0),
angle: math.pi * 2 * angle, builder: (context, angle, child) => Transform.rotate(
child: SizedBox( angle: math.pi * 2 * angle,
width: 30, child: SizedBox(
child: IconButton( width: 30,
padding: EdgeInsets.zero, child: IconButton(
tooltip: s.homeSubMenuSortBy, padding: EdgeInsets.zero,
icon: Icon( tooltip: s.homeSubMenuSortBy,
_reverse icon: Icon(
? LineIcons.hourglass_start_solid _reverse
: LineIcons.hourglass_end_solid, ? LineIcons.hourglass_start_solid
color: _reverse ? context.accentColor : null, : LineIcons.hourglass_end_solid,
color: _reverse ? context.accentColor : null,
),
iconSize: 18,
onPressed: () {
setState(() => _reverse = !_reverse);
},
), ),
iconSize: 18,
onPressed: () {
setState(() => _reverse = !_reverse);
},
), ),
), ),
), )),
)),
FutureBuilder<bool>( FutureBuilder<bool>(
future: _getHideListened(), future: _getHideListened(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -908,7 +906,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
child: _multiSelect child: _multiSelect
? Center() ? Center()
: _actionBar(context)), : _actionBar(context)),
if (_loadEpisodes) if (!widget.hide)
FutureBuilder<List<EpisodeBrief>>( FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(widget.podcastLocal, future: _getRssItem(widget.podcastLocal,
count: _top, count: _top,

View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart' as tuple;
import '../home/audioplayer.dart';
import '../state/audio_state.dart';
import '../util/extension_helper.dart';
class HidePlayerRoute extends ModalRoute<void> {
HidePlayerRoute(this.openPage, this.transitionPage, {Duration duration})
: transitionDuration = duration;
final openPage;
final transitionPage;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return Selector<AudioPlayerNotifier, tuple.Tuple2<bool, PlayerHeight>>(
selector: (_, audio) =>
tuple.Tuple2(audio.playerRunning, audio.playerHeight),
builder: (_, data, __) => Align(
alignment: Alignment.topLeft,
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
if (animation.isCompleted) {
return SizedBox.expand(
child: Material(
color: Colors.transparent,
child: Builder(
builder: (context) {
return openPage;
},
),
),
);
}
final Animation<double> curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.fastOutSlowIn.flipped,
);
final playerHeight = kMinPlayerHeight[data.item2.index];
final playerRunning = data.item1;
return SizedBox.expand(
child: Container(
child: Align(
alignment: Alignment.topLeft,
child: Transform.translate(
offset:
Offset(context.width * (1 - animation.value), 0),
child: SizedBox(
width: context.width,
height: context.height *
(playerRunning
? (1 - playerHeight / context.height)
: 1),
child: Material(
clipBehavior: Clip.antiAlias,
animationDuration: Duration.zero,
child: FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.topLeft,
child: SizedBox(
width: context.width,
height: context.height,
child: Builder(
builder: (context) {
return transitionPage;
},
),
),
),
),
),
),
),
),
);
},
),
));
}
@override
bool get maintainState => true;
@override
Color get barrierColor => null;
@override
bool get opaque => true;
@override
bool get barrierDismissible => false;
@override
String get barrierLabel => null;
@override
final Duration transitionDuration;
}
mixin Tuple2 {}