import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; import 'package:connectivity/connectivity.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:html/parser.dart'; import 'package:line_icons/line_icons.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; import '../home/audioplayer.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 '../type/episodebrief.dart'; import '../type/fireside_data.dart'; import '../type/podcastlocal.dart'; import '../util/extension_helper.dart'; import '../widgets/audiopanel.dart'; import '../widgets/custom_widget.dart'; import '../widgets/episodegrid.dart'; import '../widgets/general_dialog.dart'; import '../widgets/muiliselect_bar.dart'; import 'podcast_settings.dart'; const String kDefaultAvatar = """http://xuanmei.us/assets/default/avatar_small- 170afdc2be97fc6148b283083942d82c101d4c1061f6b28f87c8958b52664af9.jpg"""; class PodcastDetail extends StatefulWidget { PodcastDetail({Key key, @required this.podcastLocal, this.hide = false}) : super(key: key); final PodcastLocal podcastLocal; final bool hide; @override _PodcastDetailState createState() => _PodcastDetailState(); } class _PodcastDetailState extends State { final GlobalKey _refreshIndicatorKey = GlobalKey(); final GlobalKey _playerKey = GlobalKey(); final _dbHelper = DBHelper(); /// Episodes total count. int _episodeCount; /// Default layout. Layout _layout; /// If true, stop grid load animation. bool _scroll = false; double _topHeight = 0; ScrollController _controller; /// Episodes num load first time. int _top = 96; int _dataCount = 0; /// Load more episodes when scroll to bottom. bool _loadMore = false; /// Change sort by. bool _reverse = false; /// Filter type. Filter _filter = Filter.all; /// Query string String _query = ''; ///Hide listened. bool _hideListened; ///Selected episode list. List _selectedEpisodes; ///Toggle for multi-select. bool _multiSelect; bool _selectAll; bool _selectBefore; bool _selectAfter; ///Show podcast info. bool _showInfo; @override void initState() { super.initState(); _loadMore = false; _reverse = false; _controller = ScrollController(); _scroll = false; _multiSelect = false; _selectAll = false; _selectAfter = false; _selectBefore = false; _showInfo = false; } @override void dispose() { _controller.dispose(); super.dispose(); } Future _updateRssItem( BuildContext context, PodcastLocal podcastLocal) async { final result = await _dbHelper.updatePodcastRss(podcastLocal); if (result >= 0) { Fluttertoast.showToast( msg: context.s.updateEpisodesCount(result), gravity: ToastGravity.TOP, ); } if (result > 0) { var autoDownload = await _dbHelper.getAutoDownload(podcastLocal.id); if (autoDownload) { var downloader = Provider.of(context, listen: false); var result = await Connectivity().checkConnectivity(); var autoDownloadStorage = KeyValueStorage(autoDownloadNetworkKey); var autoDownloadNetwork = await autoDownloadStorage.getInt(); if (autoDownloadNetwork == 1) { var episodes = await _dbHelper.getNewEpisodes(podcastLocal.id); // For safety if (episodes.length < 100) { for (var episode in episodes) { downloader.startTask(episode, showNotification: false); } } } else if (result == ConnectivityResult.wifi) { var episodes = await _dbHelper.getNewEpisodes(podcastLocal.id); //For safety if (episodes.length < 100) { for (var episode in episodes) { downloader.startTask(episode, showNotification: false); } } } } } else if (result != 0) { Fluttertoast.showToast( msg: context.s.updateFailed, gravity: ToastGravity.TOP, ); } if (mounted && result > 0) setState(() {}); } Future> _getRssItem(PodcastLocal podcastLocal, {int count, bool reverse, Filter filter, String query}) async { var episodes = []; _episodeCount = await _dbHelper.getPodcastCounts(podcastLocal.id); final layoutStorage = KeyValueStorage(podcastLayoutKey); final hideListenedStorage = KeyValueStorage(hideListenedKey); final index = await layoutStorage.getInt(defaultValue: 1); if (_layout == null) _layout = Layout.values[index]; if (_hideListened == null) { _hideListened = await hideListenedStorage.getBool(defaultValue: false); } episodes = await _dbHelper.getRssItem(podcastLocal.id, count, reverse: reverse, filter: filter, query: query, hideListened: _hideListened); _dataCount = episodes.length; return episodes; } Future>> _getHosts( PodcastLocal podcastLocal) async { if (!podcastLocal.provider.contains('fireside')) return Tuple2('', []); var data = FiresideData(podcastLocal.id, podcastLocal.link); await data.getData(); var backgroundImage = data.background; var hosts = data.hosts; return Tuple2(backgroundImage, hosts); } Future _getLayout() async { var storage = KeyValueStorage(podcastLayoutKey); var index = await storage.getInt(defaultValue: 1); return index; } Future _getHideListened() async { var hideListenedStorage = KeyValueStorage(hideListenedKey); var hideListened = await hideListenedStorage.getBool(defaultValue: false); return hideListened; } Future _checkPodcast() async { final exist = await _dbHelper.checkPodcast(widget.podcastLocal.rssUrl); if (exist == '') { Navigator.of(context).pop(); } } Future _getNewCount() async { return await _dbHelper.getPodcastUpdateCounts(widget.podcastLocal.id); } Future _removePodcastNewMark() async { await _dbHelper.removePodcastNewMark(widget.podcastLocal.id); } Widget _podcastInfo(BuildContext context) { return Container( height: 170, padding: EdgeInsets.only(top: 50, left: 80, right: 130), alignment: Alignment.topLeft, child: Text( widget.podcastLocal.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: context.textTheme.headline5.copyWith(color: Colors.white), ), ); } Widget _podcastLink( {String title, Widget child, VoidCallback onTap, Color backgroundColor}) { return Container( padding: EdgeInsets.fromLTRB(5, 10, 5, 0), width: 60.0, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: CircleAvatar( radius: 20, child: child, backgroundColor: backgroundColor.withOpacity(0.5), ), ), SizedBox(height: 4), Text( title, style: context.textTheme.subtitle2, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.fade, ), ], )); } Widget _hostsList(BuildContext context, PodcastLocal podcastLocal) { return Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ FutureBuilder( future: _getHosts(podcastLocal), builder: (context, snapshot) { return Container( width: double.infinity, alignment: Alignment.centerLeft, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _podcastLink( title: 'Link', child: Icon(Icons.link, size: 30), backgroundColor: Colors.green[600], onTap: () => widget.podcastLocal.link.launchUrl), _podcastLink( title: 'Rss', child: Icon(LineIcons.rssSquare, size: 30), backgroundColor: Colors.blue[600], onTap: () => widget.podcastLocal.rssUrl.launchUrl), if (widget.podcastLocal.funding.isNotEmpty) for (var funding in widget.podcastLocal.funding) _podcastLink( title: 'Donate', child: Icon( funding.contains( 'paypal', ) ? LineIcons.paypal : LineIcons.donate, size: 30), backgroundColor: Colors.red[600], onTap: () => funding.launchUrl), if (snapshot.hasData) ...snapshot.data.item2 .map((host) { final image = host.image == kDefaultAvatar ? kDefaultAvatar : host.image; return Container( padding: EdgeInsets.fromLTRB(5, 10, 5, 0), width: 60.0, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ CachedNetworkImage( imageUrl: image, progressIndicatorBuilder: (context, url, downloadProgress) => CircleAvatar( backgroundColor: Colors.cyan[600].withOpacity(0.5), child: SizedBox( width: 30, height: 2, child: LinearProgressIndicator( value: downloadProgress.progress), ), ), errorWidget: (context, url, error) => CircleAvatar( backgroundColor: Colors.grey[400], backgroundImage: AssetImage('assets/fireside.jpg'), ), imageBuilder: (context, hostImage) => CircleAvatar( backgroundColor: Colors.grey[400], backgroundImage: hostImage), ), SizedBox(height: 4), Text( host.name, style: context.textTheme.subtitle2, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.fade, ), ], ), ); }) .toList() .cast() ]), ), ); }), Container( padding: EdgeInsets.fromLTRB(15, 10, 15, 10), alignment: Alignment.topLeft, color: context.scaffoldBackgroundColor, child: AboutPodcast(podcastLocal: widget.podcastLocal), ), ], ); } Widget _customPopupMenu({ Widget child, String tooltip, List> itemBuilder, Function(int) onSelected, }) => Material( key: UniqueKey(), color: Colors.transparent, borderRadius: BorderRadius.circular(100), clipBehavior: Clip.hardEdge, child: PopupMenuButton( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 1, tooltip: tooltip, child: child, itemBuilder: (context) => itemBuilder, onSelected: (value) => onSelected(value), ), ); Widget _actionBar(BuildContext context) { final s = context.s; return SizedBox( height: 30, child: Row( children: [ SizedBox(width: 15), _customPopupMenu( tooltip: s.filter, child: Container( height: 30, decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), border: Border.all(color: context.primaryColorDark)), padding: EdgeInsets.symmetric(horizontal: 10), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(s.filter), SizedBox(width: 5), Icon( LineIcons.filter, color: _filter != Filter.all ? context.accentColor : null, size: 18, ) ], ), ), itemBuilder: [ PopupMenuItem( value: 0, child: Row( children: [ Text(s.all), Spacer(), if (_filter == Filter.all) DotIndicator(), ], ), ), PopupMenuItem( value: 1, child: Row( children: [ Text(s.homeTabMenuFavotite), Spacer(), if (_filter == Filter.liked) DotIndicator() ], ), ), PopupMenuItem( value: 2, child: Row( children: [ Text(s.downloaded), Spacer(), if (_filter == Filter.downloaded) DotIndicator() ], ), ), PopupMenuItem( value: 3, child: Container( padding: EdgeInsets.only(top: 5, bottom: 5, left: 2, right: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), border: Border.all( width: 2, color: context.textColor.withOpacity(0.2))), child: _query == '' ? Row( children: [ Text(s.search, style: TextStyle( color: context.textColor .withOpacity(0.4))), Spacer() ], ) : Row( children: [ Expanded( child: Text(_query, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: context.accentColor)), ), ], ), ), ), ], onSelected: (value) { switch (value) { case 0: if (_filter != Filter.all) { setState(() { _filter = Filter.all; _query = ''; }); } break; case 1: if (_filter != Filter.liked) { setState(() { _query = ''; _filter = Filter.liked; }); } break; case 2: if (_filter != Filter.downloaded) { setState(() { _query = ''; _filter = Filter.downloaded; }); } break; case 3: showGeneralDialog( context: context, barrierDismissible: true, barrierLabel: MaterialLocalizations.of(context) .modalBarrierDismissLabel, barrierColor: Colors.black54, transitionDuration: const Duration(milliseconds: 200), pageBuilder: (context, animaiton, secondaryAnimation) => SearchEpisode( onSearch: (query) { setState(() { _query = query; _filter = Filter.search; }); }, )); break; default: } }), Spacer(), FutureBuilder( future: _getNewCount(), initialData: 0, builder: (context, snapshot) { return snapshot.data != 0 ? Padding( padding: EdgeInsets.symmetric(horizontal: 4), child: Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(100), child: SizedBox( width: 30, child: IconButton( padding: EdgeInsets.only(bottom: 5), tooltip: s.removeNewMark, icon: Container( height: 18, width: 18, child: CustomPaint( painter: RemoveNewFlagPainter( context.textTheme.bodyText1.color, Colors.red))), onPressed: () async { await _removePodcastNewMark(); if (mounted) { setState(() {}); } }), ), ), ) : Center(); }), if (!widget.hide) Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(100), child: TweenAnimationBuilder( duration: Duration(milliseconds: 500), curve: Curves.easeInOutQuart, tween: Tween(begin: 0.0, end: 1.0), builder: (context, angle, child) => Transform.rotate( angle: math.pi * 2 * angle, child: SizedBox( width: 30, child: IconButton( padding: EdgeInsets.zero, tooltip: s.homeSubMenuSortBy, icon: Icon( _reverse ? LineIcons.hourglassStart : LineIcons.hourglassEnd, color: _reverse ? context.accentColor : null, ), iconSize: 18, onPressed: () { setState(() => _reverse = !_reverse); }, ), ), ), )), FutureBuilder( future: _getHideListened(), builder: (context, snapshot) { if (_hideListened == null) { _hideListened = snapshot.data; } return Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(100), child: IconButton( icon: SizedBox( width: 30, height: 30, child: HideListened( hideListened: _hideListened ?? false, ), ), onPressed: () { setState(() => _hideListened = !_hideListened); }, )); }), FutureBuilder( future: _getLayout(), builder: (context, snapshot) { if (_layout == null && snapshot.data != null) { _layout = Layout.values[snapshot.data]; } return Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(100), child: LayoutButton( layout: _layout ?? Layout.two, onPressed: (layout) => setState(() { _layout = layout; }), ), ); }), Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(100), child: IconButton( icon: SizedBox( width: 20, height: 10, child: CustomPaint( painter: MultiSelectPainter(color: context.accentColor)), ), onPressed: () { setState(() { _top = -1; _selectedEpisodes = []; _multiSelect = true; }); }, )), SizedBox(width: 10) ], )); } @override Widget build(BuildContext context) { final color = widget.podcastLocal.primaryColor.colorizedark(); final s = context.s; return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Brightness.dark, systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness, ), child: WillPopScope( onWillPop: () { if (_playerKey.currentState != null && _playerKey.currentState.initSize > 100) { _playerKey.currentState.backToMini(); return Future.value(false); } else { return Future.value(true); } }, child: Scaffold( body: SafeArea( top: false, child: RefreshIndicator( key: _refreshIndicatorKey, displacement: context.paddingTop + 40, color: context.accentColor, onRefresh: () async { await _updateRssItem(context, widget.podcastLocal); }, child: Stack( children: [ Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: ScrollConfiguration( behavior: NoGrowBehavior(), child: CustomScrollView( controller: _controller ..addListener(() async { if (_controller.offset == _controller.position.maxScrollExtent && _dataCount == _top) { if (mounted) { setState(() => _loadMore = true); } await Future.delayed(Duration(seconds: 3)); if (mounted && _loadMore) { setState(() { _top = _top + 36; _loadMore = false; }); } } if (_controller.offset > 0 && mounted && !_scroll) { setState(() => _scroll = true); } }), physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverAppBar( brightness: Brightness.dark, actions: [ IconButton( icon: Icon(Icons.more_vert), splashRadius: 20, tooltip: s.menu, onPressed: () => generalSheet( context, title: widget.podcastLocal.title, child: PodcastSetting( podcastLocal: widget.podcastLocal), ).then((value) { _checkPodcast(); if (mounted) setState(() {}); }), ), ], elevation: 0, iconTheme: IconThemeData( color: Colors.white, ), expandedHeight: math.max(130 + context.paddingTop, 180), backgroundColor: color, floating: true, pinned: true, leading: CustomBackButton(), flexibleSpace: LayoutBuilder( builder: (context, constraints) { _topHeight = constraints.biggest.height; return FlexibleSpaceBar( background: Stack( children: [ Padding( padding: EdgeInsets.only( top: 120 + context.paddingTop), child: InkWell( onTap: () => setState( () => _showInfo = !_showInfo), child: Container( padding: EdgeInsets.only( left: 80, right: 130), color: Colors.white10, alignment: Alignment.centerLeft, child: Row( children: [ Expanded( child: Column( mainAxisAlignment: MainAxisAlignment .start, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment .start, children: [ Text( widget.podcastLocal .author ?? '', maxLines: 1, overflow: TextOverflow .ellipsis, style: TextStyle( color: Colors .white)), if (widget .podcastLocal .provider .isNotEmpty) Text( s.hostedOn(widget .podcastLocal .provider), maxLines: 1, style: TextStyle( color: Colors .white), ), ], ), ), UpDownIndicator( status: _showInfo, color: Colors.white), ], ), ), ), ), Container( alignment: Alignment.bottomRight, padding: EdgeInsets.only( right: 20, bottom: 10), child: Container( height: 100, width: 100, decoration: BoxDecoration( border: Border.all( color: Colors.white, width: 2), ), child: Image.file( File( "${widget.podcastLocal.imagePath}"), errorBuilder: (context, _, __) { return ColoredBox( color: color, child: Icon(Icons.error)); }, ), ), ), Align( alignment: Alignment.center, child: _podcastInfo(context), ), ], ), title: _topHeight < 70 + context.paddingTop ? SizedBox( width: context.width * 4 / 5, child: Text( widget.podcastLocal.title, maxLines: 1, overflow: TextOverflow.clip, style: TextStyle( color: Colors.white)), ) : Center(), ); }), ), SliverToBoxAdapter( child: _showInfo ? _hostsList(context, widget.podcastLocal) : SizedBox(height: 10), ), SliverToBoxAdapter( child: _multiSelect ? Center() : _actionBar(context)), if (!widget.hide) FutureBuilder>( future: _getRssItem(widget.podcastLocal, count: _top, reverse: _reverse, filter: _filter, query: _query), builder: (context, snapshot) { if (snapshot.hasData) { if (_selectAll) { _selectedEpisodes = snapshot.data; } if (_selectBefore) { final index = snapshot.data .indexOf(_selectedEpisodes.first); if (index != 0) { _selectedEpisodes = snapshot.data .sublist(0, index + 1); } } if (_selectAfter) { final index = snapshot.data .indexOf(_selectedEpisodes.first); _selectedEpisodes = snapshot.data.sublist(index); } return EpisodeGrid( episodes: snapshot.data, showFavorite: true, showNumber: _filter == Filter.all && !_hideListened ? true : false, layout: _layout, reverse: _reverse, episodeCount: _episodeCount, initNum: _scroll ? 0 : 12, multiSelect: _multiSelect, selectedList: _selectedEpisodes ?? [], onSelect: (value) => setState(() { _selectAll = false; _selectBefore = false; _selectAfter = false; _selectedEpisodes = value; }), ); } return SliverToBoxAdapter( child: Center()); }), SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return _loadMore ? Container( height: 2, child: LinearProgressIndicator()) : Center(); }, childCount: 1, ), ), ], ), ), ), Selector>( selector: (_, audio) => Tuple2(audio.playerRunning, audio.playerHeight), builder: (_, data, __) { var height = kMinPlayerHeight[data.item2.index]; return Column( children: [ if (_multiSelect) MultiSelectMenuBar( selectedList: _selectedEpisodes, selectAll: _selectAll, onSelectAll: (value) { setState(() { _selectAll = value; _selectAfter = false; _selectBefore = false; if (!value) { _selectedEpisodes = []; } }); }, onSelectAfter: (value) { setState(() { _selectBefore = false; _selectAfter = true; }); }, onSelectBefore: (value) { setState(() { _selectAfter = false; _selectBefore = true; }); }, onClose: (value) { setState(() { if (value) { _multiSelect = false; _selectAll = false; _selectAfter = false; _selectBefore = false; } }); }, ), SizedBox( height: data.item1 ? height : 0, ), ], ); }), ], ), Container( child: PlayerWidget( playerKey: _playerKey, )), ], ), ), ), ), ), ); } } class AboutPodcast extends StatefulWidget { final PodcastLocal podcastLocal; AboutPodcast({this.podcastLocal, Key key}) : super(key: key); @override _AboutPodcastState createState() => _AboutPodcastState(); } class _AboutPodcastState extends State { String _description; bool _load; void getDescription(String id) async { var dbHelper = DBHelper(); var description = await dbHelper.getFeedDescription(id); if (description == null || description.isEmpty) { _description = ''; } else { var doc = parse(description); _description = parse(doc.body.text).documentElement.text; } if (mounted) setState(() => _load = true); } @override void initState() { super.initState(); _load = false; getDescription(widget.podcastLocal.id); } @override Widget build(BuildContext context) { return !_load ? Center() : Linkify( text: _description, onOpen: (link) { link.url.launchUrl; }, linkStyle: TextStyle( color: Theme.of(context).accentColor, decoration: TextDecoration.underline, textBaseline: TextBaseline.ideographic), ); // LayoutBuilder( // builder: (context, size) { // final span = TextSpan(text: _description); // final tp = TextPainter( // text: span, maxLines: 3, textDirection: TextDirection.ltr); // tp.layout(maxWidth: size.maxWidth); // if (tp.didExceedMaxLines) { // return GestureDetector( // onTap: () { // setState(() => _expand = !_expand); // }, // child: !_expand // ? Column( // mainAxisAlignment: MainAxisAlignment.start, // mainAxisSize: MainAxisSize.min, // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Linkify( // onOpen: (link) { // link.url.launchUrl; // }, // text: _description, // linkStyle: TextStyle( // color: Theme.of(context).accentColor, // decoration: TextDecoration.underline, // textBaseline: TextBaseline.ideographic), // maxLines: 3, // overflow: TextOverflow.ellipsis, // ), // ], // ) // : Linkify( // onOpen: (link) { // link.url.launchUrl; // }, // text: _description, // linkStyle: TextStyle( // color: Theme.of(context).accentColor, // decoration: TextDecoration.underline, // textBaseline: TextBaseline.ideographic), // ), // ); // } else { // return Linkify( // text: _description, // onOpen: (link) { // link.url.launchUrl; // }, // linkStyle: TextStyle( // color: Theme.of(context).accentColor, // decoration: TextDecoration.underline, // textBaseline: TextBaseline.ideographic), // ); // } // }, // ); } } class SearchEpisode extends StatefulWidget { SearchEpisode({this.onSearch, Key key}) : super(key: key); final ValueChanged onSearch; @override _SearchEpisodeState createState() => _SearchEpisodeState(); } class _SearchEpisodeState extends State { TextEditingController _controller; String _query; @override void initState() { super.initState(); _controller = TextEditingController(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final s = context.s; return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Theme.of(context).brightness == Brightness.light ? Color.fromRGBO(113, 113, 113, 1) : Color.fromRGBO(5, 5, 5, 1), ), child: AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), elevation: 1, contentPadding: const EdgeInsets.symmetric(horizontal: 20), titlePadding: const EdgeInsets.all(20), actionsPadding: EdgeInsets.zero, actions: [ FlatButton( splashColor: context.accentColor.withAlpha(70), onPressed: () => Navigator.of(context).pop(), child: Text( s.cancel, textAlign: TextAlign.end, style: TextStyle(color: Colors.grey[600]), ), ), FlatButton( splashColor: context.accentColor.withAlpha(70), onPressed: () { if ((_query ?? '').isNotEmpty) { widget.onSearch(_query); Navigator.of(context).pop(); } }, child: Text(s.confirm, style: TextStyle(color: context.accentColor)), ) ], title: SizedBox(width: context.width - 160, child: Text(s.search)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 10), hintText: s.searchEpisode, hintStyle: TextStyle(fontSize: 18), filled: true, focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: context.accentColor, width: 2.0), ), enabledBorder: UnderlineInputBorder( borderSide: BorderSide(color: context.accentColor, width: 2.0), ), ), cursorRadius: Radius.circular(2), autofocus: true, maxLines: 1, controller: _controller, onChanged: (value) { setState(() => _query = value); }, ), ], ), ), ); } }