542 lines
22 KiB
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);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|