import 'dart:developer' as developer; import 'package:dio/dio.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:intl/intl.dart'; import 'package:line_icons/line_icons.dart'; import 'package:provider/provider.dart'; import 'package:webfeed/webfeed.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../state/podcast_group.dart'; import '../type/play_histroy.dart'; import '../type/search_api/searchpodcast.dart'; import '../type/sub_history.dart'; import '../util/extension_helper.dart'; import '../widgets/custom_widget.dart'; class PlayedHistory extends StatefulWidget { @override _PlayedHistoryState createState() => _PlayedHistoryState(); } class _PlayedHistoryState extends State with SingleTickerProviderStateMixin { /// Get play history. Future> _getPlayHistory(int top) async { var dbHelper = DBHelper(); List playHistory; playHistory = await dbHelper.getPlayHistory(top); for (var record in playHistory) { await record.getEpisode(); } return playHistory; } bool _loadMore = false; Future _loadMoreData() async { if (mounted) { setState(() { _loadMore = true; }); } await Future.delayed(Duration(milliseconds: 500)); if (mounted) { setState(() { _top = _top + 10; _loadMore = false; }); } } int _top = 10; Future> getSubHistory() async { var dbHelper = DBHelper(); return await dbHelper.getSubHistory(); } TabController _controller; List list = const [0, 1, 2, 3, 4, 5, 6]; Future> getData() async { var dbHelper = DBHelper(); var stats = []; for (var day in list) { var mins = await dbHelper.listenMins(7 - day); stats.add(FlSpot(day.toDouble(), mins)); } return stats; } Future recoverSub(BuildContext context, String url) async { Fluttertoast.showToast( msg: context.s.toastPodcastRecovering, gravity: ToastGravity.BOTTOM, ); var subscribeWorker = context.watch(); try { var options = BaseOptions( connectTimeout: 10000, receiveTimeout: 10000, ); var response = await Dio(options).get(url); var p = RssFeed.parse(response.data); var podcast = OnlinePodcast( rss: url, title: p.title, publisher: p.author, description: p.description, image: p.itunes.image.href); var item = SubscribeItem(podcast.rss, podcast.title, imgUrl: podcast.image, group: 'Home'); subscribeWorker.setSubscribeItem(item); } catch (e) { developer.log(e.toString(), name: 'Recover podcast error'); Fluttertoast.showToast( msg: context.s.toastRecoverFailed, gravity: ToastGravity.BOTTOM, ); } } @override void initState() { super.initState(); _controller = TabController(length: 2, vsync: this); } @override void dispose() { _controller.dispose(); super.dispose(); } double top = 0; @override Widget build(BuildContext context) { final s = context.s; return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness, ), child: Scaffold( backgroundColor: context.primaryColor, body: SafeArea( child: NestedScrollView( headerSliverBuilder: (context, innerBoxScrolled) { return [ SliverAppBar( backgroundColor: Theme.of(context).primaryColor, leading: CustomBackButton(), elevation: 0, expandedHeight: 260, floating: false, pinned: true, flexibleSpace: LayoutBuilder( builder: (context, constraints) { top = constraints.biggest.height; return FlexibleSpaceBar( title: top < 70 + MediaQuery.of(context).padding.top ? Text( s.settingsHistory, ) : Center(), background: Padding( padding: EdgeInsets.only( top: 50, left: 20, right: 20, bottom: 20), child: FutureBuilder>( future: getData(), builder: (context, snapshot) { return snapshot.hasData ? HistoryChart(snapshot.data) : Center(); }), ), ); }, ), ), SliverPersistentHeader( delegate: _SliverAppBarDelegate( TabBar( controller: _controller, indicatorColor: context.accentColor, labelColor: context.textColor, labelStyle: context.textTheme.headline6, tabs: [ Tab( child: Text(s.listen), ), Tab( child: Text(s.subscribe), ) ], ), context.primaryColor), pinned: true, ), ]; }, body: TabBarView(controller: _controller, children: [ FutureBuilder>( future: _getPlayHistory(_top), builder: (context, snapshot) { var width = context.width; return snapshot.hasData ? NotificationListener( onNotification: (scrollInfo) { if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && snapshot.data.length == _top) { if (!_loadMore) { _loadMoreData(); } } return true; }, child: ListView.builder( scrollDirection: Axis.vertical, itemCount: snapshot.data.length + 1, itemBuilder: (context, index) { if (index == snapshot.data.length) { return SizedBox( height: 2, child: _loadMore ? LinearProgressIndicator() : Center()); } else { var seekValue = snapshot.data[index].seekValue; var seconds = snapshot.data[index].seconds; return Container( padding: const EdgeInsets.symmetric(vertical: 5), color: context.scaffoldBackgroundColor, child: Column( children: [ ListTile( title: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( DateFormat.yMd() .add_jm() .format(snapshot .data[index].playdate), style: TextStyle( color: context.textColor .withOpacity(0.8), fontSize: 15, fontStyle: FontStyle.italic), ), Text( snapshot.data[index].title, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), subtitle: Row( children: [ Icon( Icons.timelapse, color: Colors.grey[400], ), Container( height: 2, decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors .grey[400], width: 2.0))), width: width * seekValue < (width - 120) ? width * seekValue : width - 120, ), Padding( padding: EdgeInsets.symmetric( horizontal: 2), ), Container( width: 50, alignment: Alignment.center, decoration: BoxDecoration( color: context.accentColor, borderRadius: BorderRadius.all( Radius.circular( 10))), padding: EdgeInsets.all(2), child: Text( seconds == 0 && seekValue == 1 ? s.mark : seconds.toInt().toTime, style: TextStyle( color: Colors.white), ), ), ], ), ), ], ), ); } }), ) : Center( child: SizedBox( height: 25, width: 25, child: CircularProgressIndicator()), ); }, ), FutureBuilder>( future: getSubHistory(), builder: (context, snapshot) { return snapshot.hasData ? ListView.builder( // shrinkWrap: true, scrollDirection: Axis.vertical, itemCount: snapshot.data.length, itemBuilder: (context, index) { var _status = snapshot.data[index].status; return Container( color: context.scaffoldBackgroundColor, child: Column( children: [ ListTile( enabled: _status, title: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( DateFormat.yMd().add_jm().format( snapshot.data[index].subDate), style: TextStyle( color: context.textColor .withOpacity(0.8), fontSize: 15, fontStyle: FontStyle.italic), ), Text(snapshot.data[index].title), ], ), subtitle: _status ? Text(s.daysAgo(DateTime.now() .difference( snapshot.data[index].subDate) .inDays)) : Text( s.removedAt(DateFormat.yMd() .add_jm() .format(snapshot .data[index].delDate)), style: TextStyle(color: Colors.red), ), trailing: !_status ? Material( color: Colors.transparent, child: IconButton( tooltip: s.recoverSubscribe, icon: Icon(LineIcons .trash_restore_alt_solid), onPressed: () => recoverSub( context, snapshot.data[index].rssUrl), ), ) : null, ), Divider( height: 2, ) ], ), ); }) : Center( child: SizedBox( height: 25, width: 25, child: CircularProgressIndicator()), ); }, ), ]), ), ), ), ); } } class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate(this._tabBar, this._color); final Color _color; final TabBar _tabBar; @override double get minExtent => _tabBar.preferredSize.height; @override double get maxExtent => _tabBar.preferredSize.height; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( color: _color, child: _tabBar, ); } @override bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { return true; } } class HistoryChart extends StatelessWidget { final List stats; HistoryChart(this.stats); @override Widget build(BuildContext context) { return SafeArea( child: LineChart( LineChartData( backgroundColor: Colors.transparent, gridData: FlGridData( show: true, drawHorizontalLine: false, getDrawingHorizontalLine: (value) { return value % 1000 == 0 ? FlLine( color: context.brightness == Brightness.light ? Colors.grey[400] : Colors.grey[700], strokeWidth: 0, ) : FlLine(color: Colors.transparent); }, ), titlesData: FlTitlesData( show: true, bottomTitles: SideTitles( getTextStyles: (i) => TextStyle( color: const Color(0xff67727d), fontWeight: FontWeight.bold, fontSize: 12, ), showTitles: true, reservedSize: 10, getTitles: (value) { return DateFormat.E().format(DateTime.now() .subtract(Duration(days: (7 - value.toInt())))); }, margin: 5, ), leftTitles: SideTitles( showTitles: true, getTextStyles: (s) => TextStyle( color: const Color(0xff67727d), fontWeight: FontWeight.bold, fontSize: 12, ), getTitles: (value) { return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : ''; }, reservedSize: 20, margin: 5, ), ), borderData: FlBorderData( show: false, border: Border( left: BorderSide(color: Colors.red, width: 2), )), lineTouchData: LineTouchData( enabled: true, touchTooltipData: LineTouchTooltipData( tooltipBgColor: context.scaffoldBackgroundColor, fitInsideHorizontally: true, getTooltipItems: (touchedBarSpots) { return touchedBarSpots.map((barSpot) { return LineTooltipItem(context.s.minsCount(barSpot.y.toInt()), context.textTheme.subtitle1); }).toList(); }, ), getTouchedSpotIndicator: (barData, spotIndexes) { return spotIndexes.map((spotIndex) { return TouchedSpotIndicatorData( FlLine(color: Colors.transparent), FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 3, color: context.accentColor, strokeWidth: 4, strokeColor: context.primaryColor); })); }).toList(); }, ), lineBarsData: [ LineChartBarData( spots: stats, isCurved: true, colors: [context.accentColor], preventCurveOverShooting: true, barWidth: 3, isStrokeCapRound: true, belowBarData: BarAreaData( show: true, gradientFrom: Offset(0, 0), gradientTo: Offset(0, 1), gradientColorStops: [ 0.3, 0.8, 0.99 ], colors: [ context.accentColor.withOpacity(0.6), context.accentColor.withOpacity(0.1), context.accentColor.withOpacity(0) ]), dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 2, color: context.primaryColor, strokeWidth: 3, strokeColor: context.accentColor); }), ), ], ), ), ); } }