Podcast page transition with fixed player.
This commit is contained in:
parent
daaeb7c8c1
commit
54268cf8b9
|
@ -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 ==
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {}
|
Loading…
Reference in New Issue