import 'dart:async'; import 'dart:io'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter/material.dart' hide NestedScrollView; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:line_icons/line_icons.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; import '../local_storage/key_value_storage.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../state/audio_state.dart'; import '../state/download_state.dart'; import '../state/podcast_group.dart'; import '../state/setting_state.dart'; import '../type/episodebrief.dart'; import '../type/playlist.dart'; import '../util/audiopanel.dart'; import '../util/custom_popupmenu.dart'; import '../util/custom_widget.dart'; import '../util/episodegrid.dart'; import '../util/extension_helper.dart'; import 'audioplayer.dart'; import 'download_list.dart'; import 'home_groups.dart'; import 'home_menu.dart'; import 'import_ompl.dart'; import 'playlist.dart'; import 'search_podcast.dart'; const String addFeature = 'addFeature'; const String menuFeature = 'menuFeature'; const String playlistFeature = 'playlistFeature'; const String longTapFeature = 'longTapFeature'; const String groupsFeature = 'groupsFeature'; const String podcastFeature = 'podcastFeature'; class Home extends StatefulWidget { @override _HomeState createState() => _HomeState(); } class _HomeState extends State with SingleTickerProviderStateMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _playerKey = GlobalKey(); TabController _controller; Decoration _getIndicator(BuildContext context) { return UnderlineTabIndicator( borderSide: BorderSide(color: Theme.of(context).accentColor, width: 3), insets: EdgeInsets.only( left: 10.0, right: 10.0, top: 10.0, )); } final _androidAppRetain = MethodChannel("android_app_retain"); var feature1OverflowMode = OverflowMode.clipContent; var feature1EnablePulsingAnimation = false; @override void initState() { super.initState(); _controller = TabController(length: 3, vsync: this); FeatureDiscovery.isDisplayed(context, addFeature).then((value) { if (!value) { WidgetsBinding.instance.addPostFrameCallback((_) { FeatureDiscovery.discoverFeatures( context, const { addFeature, menuFeature, playlistFeature, groupsFeature, podcastFeature, }, ); }); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } double top = 0; @override Widget build(BuildContext context) { var width = MediaQuery.of(context).size.width; var height = (width - 20) / 3 + 140; var settings = Provider.of(context, listen: false); final s = context.s; return AnnotatedRegion( value: SystemUiOverlayStyle( systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness, systemNavigationBarColor: Theme.of(context).primaryColor, ), child: Scaffold( key: _scaffoldKey, body: WillPopScope( onWillPop: () async { if (_playerKey.currentState != null && _playerKey.currentState.initSize > 100) { _playerKey.currentState.backToMini(); return false; } else if (Platform.isAndroid) { _androidAppRetain.invokeMethod('sendToBackground'); return false; } else { return true; } }, child: SafeArea( child: Stack( children: [ Column( children: [ Expanded( child: NestedScrollView( innerScrollPositionKeyBuilder: () { return Key('tab${_controller.index}'); }, pinnedHeaderSliverHeightBuilder: () => 50, headerSliverBuilder: (context, innerBoxScrolled) { return [ SliverToBoxAdapter( child: Column( children: [ SizedBox( height: 50.0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DescribedFeatureOverlay( featureId: addFeature, tapTarget: Icon(Icons.add_circle_outline), title: Text(s.featureDiscoverySearch), backgroundColor: Colors.cyan[600], overflowMode: feature1OverflowMode, onDismiss: () { return Future.value(true); }, description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(s .featureDiscoverySearchDes), FlatButton( color: Colors.cyan[500], padding: const EdgeInsets.all(0), child: Text(s.understood, style: Theme.of(context) .textTheme .button .copyWith( color: Colors .white)), onPressed: () async => FeatureDiscovery .completeCurrentStep( context), ), FlatButton( color: Colors.cyan[500], padding: const EdgeInsets.all(0), child: Text(s.dismiss, style: Theme.of(context) .textTheme .button .copyWith( color: Colors .white)), onPressed: () => FeatureDiscovery .dismissAll(context), ), ], ), child: IconButton( tooltip: s.add, icon: const Icon( Icons.add_circle_outline), onPressed: () async { await showSearch( context: context, delegate: MyHomePageDelegate( searchFieldLabel: s.searchPodcast), ); }, ), ), GestureDetector( onTap: () => { Theme.of(context).brightness == Brightness.light ? settings.setTheme = ThemeMode.dark : settings.setTheme = ThemeMode.light }, child: Image( image: Theme.of(context) .brightness == Brightness.light ? AssetImage( 'assets/text.png') : AssetImage( 'assets/text_light.png'), height: 30, ), ), DescribedFeatureOverlay( featureId: menuFeature, tapTarget: Icon(Icons.more_vert), backgroundColor: Colors.cyan[500], onDismiss: () => Future.value(true), title: Text(s.featureDiscoveryOMPL), description: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(s .featureDiscoveryOMPLDes), FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.understood, style: Theme.of(context) .textTheme .button .copyWith( color: Colors .white)), onPressed: () async => FeatureDiscovery .completeCurrentStep( context), ), FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.dismiss, style: Theme.of(context) .textTheme .button .copyWith( color: Colors .white)), onPressed: () => FeatureDiscovery .dismissAll( context), ), ], ), child: PopupMenu()), ], ), ), Import(), ], ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return DescribedFeatureOverlay( featureId: groupsFeature, tapTarget: Center( child: Text( s.featureDiscoveryPodcast, textAlign: TextAlign.center, )), backgroundColor: Colors.cyan[500], enablePulsingAnimation: false, onDismiss: () => Future.value(true), title: Text(s.featureDiscoveryPodcastTitle), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(s.featureDiscoveryPodcastDes), Row( children: [ FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.understood, style: Theme.of(context) .textTheme .button .copyWith( color: Colors.white)), onPressed: () async => FeatureDiscovery .completeCurrentStep( context), ), Padding( padding: EdgeInsets.symmetric( horizontal: 5)), FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.dismiss, style: Theme.of(context) .textTheme .button .copyWith( color: Colors.white)), onPressed: () => FeatureDiscovery.dismissAll( context), ), ], ), ], ), child: SizedBox( height: height, width: width, child: ScrollPodcasts(), ), ); }, childCount: 1, ), ), SliverPersistentHeader( delegate: _SliverAppBarDelegate( TabBar( indicator: _getIndicator(context), isScrollable: true, indicatorSize: TabBarIndicatorSize.tab, controller: _controller, tabs: [ Tab( child: Text(s.homeTabMenuRecent), ), Tab( child: Text(s.homeTabMenuFavotite), ), Tab( child: Text(s.download), ) ], ), ), pinned: true, ), ]; }, body: ScrollConfiguration( behavior: NoGrowBehavior(), child: TabBarView( controller: _controller, children: [ NestedScrollViewInnerScrollPositionKeyWidget( Key('tab0'), DescribedFeatureOverlay( featureId: podcastFeature, tapTarget: Text( s.featureDiscoveryEpisode, textAlign: TextAlign.center), backgroundColor: Colors.cyan[500], enablePulsingAnimation: false, onDismiss: () => Future.value(true), title: Text( s.featureDiscoveryEpisodeTitle), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(s.featureDiscoveryEpisodeDes), Row( children: [ FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.understood, style: Theme.of(context) .textTheme .button .copyWith( color: Colors .white)), onPressed: () async => FeatureDiscovery .completeCurrentStep( context), ), Padding( padding: EdgeInsets.symmetric( horizontal: 5)), FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.dismiss, style: Theme.of(context) .textTheme .button .copyWith( color: Colors .white)), onPressed: () => FeatureDiscovery .dismissAll(context), ), ], ), ], ), child: _RecentUpdate())), NestedScrollViewInnerScrollPositionKeyWidget( Key('tab1'), _MyFavorite()), NestedScrollViewInnerScrollPositionKeyWidget( Key('tab2'), _MyDownload()), ], ), ), ), ), Selector( selector: (_, audio) => audio.playerRunning, builder: (_, data, __) { return Padding( padding: EdgeInsets.only(bottom: data ? 60.0 : 0), ); }), ], ), Container(child: PlayerWidget(playerKey: _playerKey)), ], ), ), ), )); } } class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate(this._tabBar); final TabBar _tabBar; @override double get minExtent => _tabBar.preferredSize.height + 2; @override double get maxExtent => _tabBar.preferredSize.height + 2; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { final s = context.s; return Container( color: context.scaffoldBackgroundColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ _tabBar, Spacer(), DescribedFeatureOverlay( featureId: playlistFeature, tapTarget: Icon(Icons.playlist_play), backgroundColor: Colors.cyan[500], title: Text(s.featureDiscoveryPlaylist), onDismiss: () => Future.value(true), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(s.featureDiscoveryPlaylistDes), FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.understood, style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white)), onPressed: () async => FeatureDiscovery.completeCurrentStep(context), ), FlatButton( color: Colors.cyan[600], padding: const EdgeInsets.all(0), child: Text(s.dismiss, style: Theme.of(context) .textTheme .button .copyWith(color: Colors.white)), onPressed: () => FeatureDiscovery.dismissAll(context), ), ], ), child: PlaylistButton()), ], ), Container(height: 2, color: context.primaryColor), ], ), ); } @override bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { return true; } } class PlaylistButton extends StatefulWidget { PlaylistButton({Key key}) : super(key: key); @override PlaylistButtonState createState() => PlaylistButtonState(); } class PlaylistButtonState extends State { bool _loadPlay; static String _stringForSeconds(int seconds) { if (seconds == null) return null; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; } _getPlaylist() async { await Provider.of(context, listen: false) .loadPlaylist(); setState(() { _loadPlay = true; }); } @override void initState() { super.initState(); _loadPlay = false; _getPlaylist(); } @override Widget build(BuildContext context) { var audio = Provider.of(context, listen: false); final s = context.s; return MyPopupMenuButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), elevation: 1, icon: Icon(Icons.playlist_play), tooltip: s.menu, itemBuilder: (context) => [ MyPopupMenuItem( height: 50, value: 1, child: Container( decoration: BoxDecoration( // color: Theme.of(context).accentColor, borderRadius: BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)), ), child: Selector>( selector: (_, audio) => Tuple3(audio.playerRunning, audio.queue, audio.lastPositin), builder: (_, data, __) => !_loadPlay ? Container( height: 8.0, ) : data.item1 || data.item2.playlist.length == 0 ? Container( height: 8.0, ) : InkWell( borderRadius: BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)), onTap: () { audio.playlistLoad(); Navigator.pop(context); }, child: Column( children: [ Padding( padding: EdgeInsets.symmetric(vertical: 5), ), Stack( alignment: Alignment.center, children: [ CircleAvatar( radius: 20, backgroundImage: FileImage(File( "${data.item2.playlist.first.imagePath}")), ), Container( height: 40.0, width: 40.0, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black12), child: Icon( Icons.play_arrow, color: Colors.white, ), ), ], ), Padding( padding: EdgeInsets.symmetric(vertical: 2), ), Container( height: 70, width: 140, child: Column( children: [ Text( _stringForSeconds(data.item3 ~/ 1000), // style: // TextStyle(color: Colors.white) ), Text( data.item2.playlist.first.title, maxLines: 2, textAlign: TextAlign.center, overflow: TextOverflow.fade, // style: TextStyle(color: Colors.white), ), ], ), ), Divider( height: 2, ), ], ), ), ), ), ), PopupMenuItem( value: 0, child: Container( padding: EdgeInsets.only(left: 10), child: Row( children: [ Icon(Icons.playlist_play), Padding( padding: EdgeInsets.symmetric(horizontal: 5.0), ), Text(s.homeMenuPlaylist), ], ), ), ), ], onSelected: (value) { if (value == 0) { Navigator.push( context, MaterialPageRoute(builder: (context) => PlaylistPage())); } else if (value == 1) {} }, ); } } class _RecentUpdate extends StatefulWidget { @override _RecentUpdateState createState() => _RecentUpdateState(); } class _RecentUpdateState extends State<_RecentUpdate> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { Future> _getRssItem(int top, List group) async { var storage = KeyValueStorage(recentLayoutKey); var index = await storage.getInt(defaultValue: 1); if (_layout == null) _layout = Layout.values[index]; var dbHelper = DBHelper(); List episodes; if (group.first == 'All') { episodes = await dbHelper.getRecentRssItem(top); } else { episodes = await dbHelper.getGroupRssItem(top, group); } return episodes; } Future _getUpdateCounts(List group) async { var dbHelper = DBHelper(); var episodes = []; if (group.first == 'All') { episodes = await dbHelper.getRecentNewRssItem(); } else { episodes = await dbHelper.getGroupNewRssItem(group); } return episodes.length; } /// Load more episodes. _loadMoreEpisode() async { if (mounted) setState(() => _loadMore = true); await Future.delayed(Duration(seconds: 3)); if (mounted) { setState(() { _top = _top + 33; _loadMore = false; }); } } /// Episodes loaded first time. int _top = 99; /// Load more episodes when scroll to bottom. bool _loadMore; /// For group fliter. String _groupName; List _group; Layout _layout; bool _scroll; @override void initState() { super.initState(); _loadMore = false; _groupName = 'All'; _group = ['All']; _scroll = false; } @override Widget build(BuildContext context) { super.build(context); var audio = Provider.of(context, listen: false); final s = context.s; return Selector( selector: (_, worker) => worker.created, builder: (context, created, child) { return FutureBuilder>( future: _getRssItem(_top, _group), builder: (context, snapshot) { return (snapshot.hasData) ? snapshot.data.length == 0 ? Padding( padding: EdgeInsets.only(top: 150), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(LineIcons.cloud_download_alt_solid, size: 80, color: Colors.grey[500]), Padding( padding: EdgeInsets.symmetric(vertical: 10)), Text( s.noEpisodeRecent, style: TextStyle(color: Colors.grey[500]), ) ], ), ) : NotificationListener( onNotification: (scrollInfo) { if (scrollInfo is ScrollStartNotification && mounted && !_scroll) { setState(() => _scroll = true); } if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && snapshot.data.length == _top) { _loadMoreEpisode(); } return true; }, child: CustomScrollView( key: PageStorageKey('update'), physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverToBoxAdapter( child: Container( height: 40, color: context.primaryColor, child: Material( color: Colors.transparent, child: Row( children: [ Consumer( builder: (context, groupList, child) => Material( color: Colors.transparent, child: PopupMenuButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular( 10))), elevation: 1, tooltip: s.groupFilter, child: Container( padding: EdgeInsets.symmetric( horizontal: 20), height: 50, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(_groupName == 'All' ? s.all : _groupName), Padding( padding: EdgeInsets .symmetric( horizontal: 5), ), Icon( LineIcons .filter_solid, size: 18, ) ], )), itemBuilder: (context) => [ PopupMenuItem( child: Row(children: [ Text(s.all), Spacer(), if (_groupName == 'All') DotIndicator() ]), value: 'All') ]..addAll(groupList.groups .map< PopupMenuEntry< String>>( (e) => PopupMenuItem( value: e.name, child: Row( children: [ Text(e.name), Spacer(), if (e.name == _groupName) DotIndicator() ], ))) .toList()), onSelected: (value) { if (value == 'All') { setState(() { _groupName = 'All'; _group = ['All']; }); } else { for (var group in groupList.groups) { if (group.name == value) { setState(() { _groupName = value; _group = group .podcastList; }); } } } }, ), ), ), Spacer(), FutureBuilder( future: _getUpdateCounts(_group), initialData: 0, builder: (context, snapshot) { return snapshot.data != 0 ? Material( color: Colors .transparent, child: IconButton( tooltip: s .addNewEpisodeTooltip, icon: SizedBox( height: 16, width: 21, child: CustomPaint( painter: AddToPlaylistPainter( context .textTheme .bodyText1 .color, Colors .red))), onPressed: () async { await audio .addNewEpisode( _group); if (mounted) { setState( () {}); } Fluttertoast .showToast( msg: _groupName == 'All' ? s.addNewEpisodeAll( snapshot .data) : s.addEpisodeGroup( _groupName, snapshot .data), gravity: ToastGravity .BOTTOM, ); }), ) : Material( color: Colors .transparent, child: IconButton( tooltip: s .addNewEpisodeTooltip, icon: SizedBox( height: 16, width: 21, child: CustomPaint( painter: AddToPlaylistPainter( context .textColor, context .textColor, ))), onPressed: () {}), ); }), Material( color: Colors.transparent, child: LayoutButton( layout: _layout, onPressed: (layout) => setState(() { _layout = layout; }), ), ), ], ), )), ), EpisodeGrid( episodes: snapshot.data, layout: _layout, initNum: _scroll ? 0 : 12, ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return _loadMore ? Container( height: 2, child: LinearProgressIndicator()) : Center(); }, childCount: 1, ), ), ])) : Center(); }, ); }); } @override bool get wantKeepAlive => true; } class _MyFavorite extends StatefulWidget { @override _MyFavoriteState createState() => _MyFavoriteState(); } class _MyFavoriteState extends State<_MyFavorite> with AutomaticKeepAliveClientMixin { Future> _getLikedRssItem(int top, int sortBy) async { var storage = KeyValueStorage(favLayoutKey); var index = await storage.getInt(defaultValue: 1); if (_layout == null) _layout = Layout.values[index]; var dbHelper = DBHelper(); var episodes = await dbHelper.getLikedRssItem(top, sortBy); return episodes; } _loadMoreEpisode() async { if (mounted) setState(() => _loadMore = true); await Future.delayed(Duration(seconds: 3)); if (mounted) { setState(() { _top = _top + 33; _loadMore = false; }); } } int _top = 99; bool _loadMore; Layout _layout; int _sortBy; @override void initState() { super.initState(); _loadMore = false; _sortBy = 0; } @override Widget build(BuildContext context) { super.build(context); final s = context.s; return Selector( selector: (_, audio) => audio.episodeState, builder: (context, episodeState, child) { return FutureBuilder>( future: _getLikedRssItem(_top, _sortBy), builder: (context, snapshot) { return (snapshot.hasData) ? snapshot.data.length == 0 ? Padding( padding: EdgeInsets.only(top: 150), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(LineIcons.heartbeat_solid, size: 80, color: Colors.grey[500]), Padding( padding: EdgeInsets.symmetric(vertical: 10)), Text( s.noEpisodeFavorite, style: TextStyle(color: Colors.grey[500]), ) ], ), ) : NotificationListener( onNotification: (scrollInfo) { if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && snapshot.data.length == _top) { _loadMoreEpisode(); } return true; }, child: CustomScrollView( key: PageStorageKey('favorite'), slivers: [ SliverToBoxAdapter( child: Container( height: 40, color: context.primaryColor, child: Row( children: [ Material( color: Colors.transparent, child: PopupMenuButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(10))), elevation: 1, tooltip: s.homeSubMenuSortBy, child: Container( height: 50, padding: EdgeInsets.symmetric( horizontal: 20), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(s.homeSubMenuSortBy), Padding( padding: EdgeInsets.symmetric( horizontal: 5), ), Icon( LineIcons .hourglass_start_solid, size: 18, ) ], )), itemBuilder: (context) => [ PopupMenuItem( value: 0, child: Row( children: [ Text(s.updateDate), Spacer(), if (_sortBy == 0) DotIndicator() ], ), ), PopupMenuItem( value: 1, child: Row( children: [ Text(s.likeDate), Spacer(), if (_sortBy == 1) DotIndicator() ], ), ) ], onSelected: (value) { if (value == 0) { setState(() => _sortBy = 0); } else if (value == 1) { setState(() => _sortBy = 1); } }, ), ), Spacer(), Material( color: Colors.transparent, child: LayoutButton( layout: _layout, onPressed: (layout) => setState(() { _layout = layout; }), ), ), ], )), ), EpisodeGrid( episodes: snapshot.data, layout: _layout, initNum: 0, ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return _loadMore ? Container( height: 2, child: LinearProgressIndicator()) : Center(); }, childCount: 1, ), ), ], ), ) : Center(); }, ); }); } @override bool get wantKeepAlive => true; } class _MyDownload extends StatefulWidget { @override _MyDownloadState createState() => _MyDownloadState(); } class _MyDownloadState extends State<_MyDownload> with AutomaticKeepAliveClientMixin { Layout _layout; _getLayout() async { var keyValueStorage = KeyValueStorage(downloadLayoutKey); var layout = await keyValueStorage.getInt(defaultValue: 1); if (_layout == null) { setState(() { _layout = Layout.values[layout]; }); } } @override void initState() { super.initState(); _getLayout(); } @override Widget build(BuildContext context) { super.build(context); final s = context.s; return CustomScrollView( key: PageStorageKey('download_list'), slivers: [ DownloadList(), SliverToBoxAdapter( child: _layout == null ? Center() : Container( height: 40, color: context.primaryColor, child: Row( children: [ Container( padding: EdgeInsets.symmetric(horizontal: 20), child: Text(s.downloaded)), Spacer(), Material( color: Colors.transparent, child: LayoutButton( layout: _layout, onPressed: (layout) => setState(() { _layout = layout; }), ), ), ], )), ), Consumer( builder: (_, downloader, __) { var episodes = downloader.episodeTasks .where((task) => task.status.value == 3) .toList() .map((e) => e.episode) .toList() .reversed .toList(); return episodes.length == 0 ? SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 110), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(LineIcons.download_solid, size: 80, color: Colors.grey[500]), Padding(padding: EdgeInsets.symmetric(vertical: 10)), Text( s.noEpisodeDownload, style: TextStyle(color: Colors.grey[500]), ) ], ), ), ) : EpisodeGrid( episodes: episodes, layout: _layout, initNum: 0, ); }, ), ], ); } @override bool get wantKeepAlive => true; }