tsacdop-podcast-app-android/lib/settings/history.dart

542 lines
22 KiB
Dart

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/custom_widget.dart';
import '../util/extension_helper.dart';
class PlayedHistory extends StatefulWidget {
@override
_PlayedHistoryState createState() => _PlayedHistoryState();
}
class _PlayedHistoryState extends State<PlayedHistory>
with SingleTickerProviderStateMixin {
/// Get play history.
Future<List<PlayHistory>> _getPlayHistory(int top) async {
var dbHelper = DBHelper();
List<PlayHistory> playHistory;
playHistory = await dbHelper.getPlayHistory(top);
for (var record in playHistory) {
await record.getEpisode();
}
return playHistory;
}
bool _loadMore = false;
Future<void> _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<List<SubHistory>> getSubHistory() async {
var dbHelper = DBHelper();
return await dbHelper.getSubHistory();
}
TabController _controller;
List<int> list = const [0, 1, 2, 3, 4, 5, 6];
Future<List<FlSpot>> getData() async {
var dbHelper = DBHelper();
var stats = <FlSpot>[];
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<GroupList>();
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<SystemUiOverlayStyle>(
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 <Widget>[
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<List<FlSpot>>(
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: <Widget>[
Tab(
child: Text(s.listen),
),
Tab(
child: Text(s.subscribe),
)
],
),
context.primaryColor),
pinned: true,
),
];
},
body: TabBarView(controller: _controller, children: <Widget>[
FutureBuilder<List<PlayHistory>>(
future: _getPlayHistory(_top),
builder: (context, snapshot) {
var width = context.width;
return snapshot.hasData
? NotificationListener<ScrollNotification>(
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: <Widget>[
ListTile(
title: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
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<List<SubHistory>>(
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: <Widget>[
ListTile(
enabled: _status,
title: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
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<FlSpot> 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);
}),
),
],
),
),
);
}
}