[TestFlight] v4.0.0+400010 (#254)

NEW
- [Tautulli/Graphs] Implemented graphs page
- [Tautulli/Updates] Implemented check for updates page

TWEAKS
- None

FIXES
- None
This commit is contained in:
Jagandeep Brar
2020-09-11 10:18:22 -05:00
committed by GitHub
parent dbbed187a9
commit 3ecb8e8a3c
40 changed files with 1440 additions and 351 deletions

View File

@@ -46,6 +46,7 @@ class Constants {
static const TEXT_BULLET = '';
static const TEXT_RARROW = '';
static const TEXT_LARROW = '';
static const TEXT_ELLIPSIS = '';
//UI
static const UI_ELEVATION = 0.0;
static const UI_CARD_MARGIN = EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0);
@@ -57,6 +58,7 @@ class Constants {
static const UI_FONT_SIZE_SUBHEADER = 12.0;
static const UI_FONT_SIZE_TITLE = 16.0;
static const UI_FONT_SIZE_SUBTITLE = 13.0;
static const UI_FONT_SIZE_GRAPH_LEGEND = 10.0;
//General
static const EMPTY_MAP = {};
static const EMPTY_LIST = [];

View File

@@ -18,4 +18,13 @@ class LSColors {
static Color list(int i) {
return Constants.LIST_COLOR_ICONS[i%Constants.LIST_COLOR_ICONS.length];
}
static Color graph(int i) {
switch(i) {
case 0: return LSColors.accent;
case 1: return LSColors.purple;
case 2: return LSColors.blue;
default: return LSColors.list(i);
}
}
}

View File

@@ -2,6 +2,7 @@ export 'core/constants.dart';
export 'core/database.dart';
export 'core/dialogs.dart';
export 'core/extensions.dart';
export 'core/graphs.dart';
export 'core/router.dart';
export 'core/state.dart';
export 'core/types.dart';

View File

@@ -0,0 +1,3 @@
export 'graphs/bar_graph.dart';
export 'graphs/graph.dart';
export 'graphs/line_graph.dart';

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliBarGraphHelper {
static const int BAR_COUNT = 7;
static const double BAR_WIDTH = 30.0;
TautulliBarGraphHelper._();
static List<BarChartGroupData> barGroups(BuildContext context, TautulliGraphData data) => List<BarChartGroupData>.generate(
data.categories.take(BAR_COUNT).length,
(cIndex) => BarChartGroupData(
x: cIndex,
barRods: [
BarChartRodData(
y: data.series.fold<double>(0, (value, data) => value+data.data[cIndex]),
width: BAR_WIDTH,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(Constants.UI_BORDER_RADIUS/3),
topRight: Radius.circular(Constants.UI_BORDER_RADIUS/3),
),
rodStackItems: List<BarChartRodStackItem>.generate(
data.series.length,
(sIndex) => BarChartRodStackItem(
_fromY(cIndex, sIndex, data.series),
_toY(cIndex, sIndex, data.series),
LSColors.graph(sIndex),
),
),
),
],
),
);
static BarTouchData barTouchData(BuildContext context, TautulliGraphData data) => BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: LunaSeaDatabaseValue.THEME_AMOLED.data ? Colors.black : LSColors.primary,
tooltipRoundedRadius: Constants.UI_BORDER_RADIUS,
tooltipPadding: EdgeInsets.all(8.0),
maxContentWidth: MediaQuery.of(context).size.width/1.25,
fitInsideVertically: true,
fitInsideHorizontally: true,
getTooltipItem: (group, gIndex, rod, rIndex) {
String _header = '${data.categories[gIndex]}\n\n';
String _body = '';
for(int i=0; i<rod.rodStackItems.length; i++) {
double _number = (rod?.rodStackItems[i]?.toY ?? 0)-(rod?.rodStackItems[i]?.fromY ?? 0);
String _value = data?.series[i]?.name ?? 'Unknown';
String _text = Provider.of<TautulliState>(context, listen: false).graphYAxis == TautulliGraphYAxis.PLAYS
? (_number?.truncate() ?? 0).toString()
: Duration(seconds: _number?.truncate() ?? 0).lsDuration_fullTimestamp();
_body += '$_value: $_text\n';
}
return BarTooltipItem(
(_header + _body).trim(),
TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBHEADER,
),
);
},
),
);
static double _fromY(
int cIndex,
int sIndex,
List<TautulliSeriesData> series,
) => series.take(sIndex).fold<double>(0, (value, data) => value+data.data[cIndex]);
static double _toY(
int cIndex,
int sIndex,
List<TautulliSeriesData> series,
) => series.take(sIndex+1).fold<double>(0, (value, data) => value+data.data[cIndex]);
}

View File

@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphHelper {
static const GRAPH_HEIGHT = 225.0;
static const LEGEND_HEIGHT = 26.0;
static const DEFAULT_MAX_TITLE_LENGTH = 5;
TautulliGraphHelper._();
static BarChartAlignment chartAlignment() => BarChartAlignment.spaceEvenly;
static FlGridData gridData() => FlGridData(show: false);
static FlBorderData borderData() => FlBorderData(
show: true,
border: Border.all(color: Colors.white12),
);
static FlTitlesData titlesData(TautulliGraphData data, {
int maxTitleLength = DEFAULT_MAX_TITLE_LENGTH,
bool titleOverFlowShowEllipsis = true,
}) => FlTitlesData(
leftTitles: SideTitles(showTitles: false),
rightTitles: SideTitles(showTitles: false),
topTitles: SideTitles(showTitles: false),
bottomTitles: SideTitles(
showTitles: true,
margin: 8.0,
reservedSize: 8.0,
getTitles: (value) => data.categories[value.truncate()].length > maxTitleLength+1
? [
data.categories[value.truncate()].substring(0, maxTitleLength).toUpperCase(),
if(titleOverFlowShowEllipsis) Constants.TEXT_ELLIPSIS,
].join()
: data.categories[value.truncate()].toUpperCase(),
textStyle: TextStyle(
color: Colors.white30,
fontSize: Constants.UI_FONT_SIZE_GRAPH_LEGEND,
),
),
);
static Widget createLegend(List<TautulliSeriesData> data) => Container(
child: Row(
children: List.generate(
data.length,
(index) => Padding(
child: Row(
children: [
Padding(
child: Container(
height: Constants.UI_FONT_SIZE_GRAPH_LEGEND,
width: Constants.UI_FONT_SIZE_GRAPH_LEGEND,
decoration: BoxDecoration(
color: LSColors.graph(index),
borderRadius: BorderRadius.circular(8.0),
),
),
padding: EdgeInsets.only(right: 6.0),
),
Text(
data[index].name,
style: TextStyle(
fontSize: Constants.UI_FONT_SIZE_GRAPH_LEGEND,
color: LSColors.graph(index),
),
),
],
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
),
padding: EdgeInsets.symmetric(horizontal: 6.0),
),
),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
),
height: LEGEND_HEIGHT,
);
static Widget get loadingContainer => LSCard(
child: Container(
height: GRAPH_HEIGHT+LEGEND_HEIGHT,
child: LSLoader(),
),
);
static Widget get errorContainer => LSCard(
child: Container(
height: GRAPH_HEIGHT+LEGEND_HEIGHT,
alignment: Alignment.center,
child: LSIconButton(
icon: Icons.error,
iconSize: 60.0,
color: Colors.white12,
),
),
);
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliLineGraphHelper {
TautulliLineGraphHelper._();
static FlTitlesData titlesData(TautulliGraphData data) => FlTitlesData(
leftTitles: SideTitles(showTitles: false),
rightTitles: SideTitles(showTitles: false),
topTitles: SideTitles(showTitles: false),
bottomTitles: SideTitles(
showTitles: true,
margin: 8.0,
reservedSize: 8.0,
getTitles: (value) => DateTime.tryParse((data.categories[value.truncate()])) != null
? DateFormat('dd').format(DateTime.parse((data.categories[value.truncate()])))?.toString()
: '??',
textStyle: TextStyle(
color: Colors.white30,
fontSize: Constants.UI_FONT_SIZE_GRAPH_LEGEND,
),
),
);
static List<LineChartBarData> lineBarsData(TautulliGraphData data) => List<LineChartBarData>.generate(
data.series.length,
(sIndex) => LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
barWidth: 3.0,
colors: [LSColors.graph(sIndex)],
spots: List<FlSpot>.generate(
data.series[sIndex].data.length,
(dIndex) => FlSpot(dIndex.toDouble(), data.series[sIndex].data[dIndex].toDouble()),
),
belowBarData: BarAreaData(
show: true,
colors: [LSColors.graph(sIndex).withOpacity(0.20)],
),
dotData: FlDotData(
show: true,
getDotPainter: (FlSpot spot, double xPercentage, LineChartBarData bar, int index) => FlDotCirclePainter(
radius: 2.50,
strokeColor: bar.colors[0],
color: bar.colors[0],
),
),
),
);
static LineTouchData lineTouchData(BuildContext context, TautulliGraphData data) => LineTouchData(
enabled: true,
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: LunaSeaDatabaseValue.THEME_AMOLED.data ? Colors.black : LSColors.primary,
tooltipRoundedRadius: Constants.UI_BORDER_RADIUS,
tooltipPadding: EdgeInsets.all(8.0),
maxContentWidth: MediaQuery.of(context).size.width/1.25,
fitInsideVertically: true,
fitInsideHorizontally: true,
getTooltipItems: (List<LineBarSpot> spots) => List<LineTooltipItem>.generate(
spots.length,
(index) => LineTooltipItem(
[
'${data.series[spots[index].barIndex].name}: ',
Provider.of<TautulliState>(context, listen: false).graphYAxis == TautulliGraphYAxis.PLAYS
? '${spots[index]?.y?.truncate() ?? 0}'
: '${Duration(seconds: spots[index]?.y?.truncate() ?? 0).lsDuration_fullTimestamp()}',
].join().trim(),
TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBHEADER,
),
),
),
),
getTouchedSpotIndicator: (bar, data) => List<TouchedSpotIndicatorData>.generate(
data.length,
(index) => TouchedSpotIndicatorData(
FlLine(
strokeWidth: 3.0,
color: bar.colors[0].withOpacity(0.50),
),
FlDotData(
show: true,
getDotPainter: (FlSpot spot, double xPercentage, LineChartBarData bar, int index) => FlDotCirclePainter(
radius: 5.0,
strokeColor: bar.colors[0],
color: bar.colors[0],
),
),
),
),
);
}

View File

@@ -21,6 +21,7 @@ class TautulliRouter {
TautulliLogsPlexMediaServerRoute.defineRoute(router);
TautulliLogsTautulliRoute.defineRoute(router);
// Other/More
TautulliCheckForUpdatesRoute.defineRoute(router);
TautulliGraphsRoute.defineRoute(router);
TautulliStatisticsRoute.defineRoute(router);
TautulliSyncedItemsRoute.defineRoute(router);

View File

@@ -251,17 +251,17 @@ class TautulliLocalState extends ChangeNotifier {
/// GRAPHS ///
//////////////
Future<TautulliGraphData> _playCountByDateGraph;
Future<TautulliGraphData> get playCountByDateGraph => _playCountByDateGraph;
set playCountByDateGraph(Future<TautulliGraphData> playCountByDateGraph) {
assert(playCountByDateGraph != null);
_playCountByDateGraph = playCountByDateGraph;
Future<TautulliGraphData> _dailyPlayCountGraph;
Future<TautulliGraphData> get dailyPlayCountGraph => _dailyPlayCountGraph;
set dailyPlayCountGraph(Future<TautulliGraphData> dailyPlayCountGraph) {
assert(dailyPlayCountGraph != null);
_dailyPlayCountGraph = dailyPlayCountGraph;
notifyListeners();
}
void _resetPlayCountByDateGraph(BuildContext context) {
void resetDailyPlayCountGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByDateGraph = _state.api.history.getPlaysByDate(
if(_state.api != null) _dailyPlayCountGraph = _state.api.history.getPlaysByDate(
timeRange: TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data,
yAxis: _state.graphYAxis,
);
@@ -276,7 +276,7 @@ class TautulliLocalState extends ChangeNotifier {
notifyListeners();
}
void _resetPlaysByMonthGraph(BuildContext context) {
void resetPlaysByMonthGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playsByMonthGraph = _state.api.history.getPlaysPerMonth(
timeRange: TautulliDatabaseValue.GRAPHS_MONTHS.data,
@@ -285,10 +285,192 @@ class TautulliLocalState extends ChangeNotifier {
notifyListeners();
}
void resetAllPlayPeriodGraphs(BuildContext context) {
_resetPlayCountByDateGraph(context);
_resetPlaysByMonthGraph(context);
Future<TautulliGraphData> _playCountByDayOfWeekGraph;
Future<TautulliGraphData> get playCountByDayOfWeekGraph => _playCountByDayOfWeekGraph;
set playCountByDayOfWeekGraph(Future<TautulliGraphData> playCountByDayOfWeekGraph) {
assert(playCountByDayOfWeekGraph != null);
_playCountByDayOfWeekGraph = playCountByDayOfWeekGraph;
notifyListeners();
}
void resetAllStreamInfoGraphs(BuildContext context) {}
void resetPlayCountByDayOfWeekGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByDayOfWeekGraph = _state.api.history.getPlaysByDayOfWeek(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
Future<TautulliGraphData> _playCountByTopPlatformsGraph;
Future<TautulliGraphData> get playCountByTopPlatformsGraph => _playCountByTopPlatformsGraph;
set playCountByTopPlatformsGraph(Future<TautulliGraphData> playCountByTopPlatformsGraph) {
assert(playCountByTopPlatformsGraph != null);
_playCountByTopPlatformsGraph = playCountByTopPlatformsGraph;
notifyListeners();
}
void resetPlayCountByTopPlatformsGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByTopPlatformsGraph = _state.api.history.getPlaysByTopTenPlatforms(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
Future<TautulliGraphData> _playCountByTopUsersGraph;
Future<TautulliGraphData> get playCountByTopUsersGraph => _playCountByTopUsersGraph;
set playCountByTopUsersGraph(Future<TautulliGraphData> playCountByTopUsersGraph) {
assert(playCountByTopUsersGraph != null);
_playCountByTopUsersGraph = playCountByTopUsersGraph;
notifyListeners();
}
void resetPlayCountByTopUsersGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByTopUsersGraph = _state.api.history.getPlaysByTopTenUsers(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
void resetAllPlayPeriodGraphs(BuildContext context) {
resetDailyPlayCountGraph(context);
resetPlaysByMonthGraph(context);
resetPlayCountByDayOfWeekGraph(context);
resetPlayCountByTopPlatformsGraph(context);
resetPlayCountByTopUsersGraph(context);
}
Future<TautulliGraphData> _dailyStreamTypeBreakdownGraph;
Future<TautulliGraphData> get dailyStreamTypeBreakdownGraph => _dailyStreamTypeBreakdownGraph;
set dailyStreamTypeBreakdownGraph(Future<TautulliGraphData> dailyStreamTypeBreakdownGraph) {
assert(dailyStreamTypeBreakdownGraph != null);
_dailyStreamTypeBreakdownGraph = dailyStreamTypeBreakdownGraph;
notifyListeners();
}
void resetDailyStreamTypeBreakdownGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _dailyStreamTypeBreakdownGraph = _state.api.history.getPlaysByStreamType(
timeRange: TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
Future<TautulliGraphData> _playCountBySourceResolutionGraph;
Future<TautulliGraphData> get playCountBySourceResolutionGraph => _playCountBySourceResolutionGraph;
set playCountBySourceResolutionGraph(Future<TautulliGraphData> playCountBySourceResolutionGraph) {
assert(playCountBySourceResolutionGraph != null);
_playCountBySourceResolutionGraph = playCountBySourceResolutionGraph;
notifyListeners();
}
void resetPlayCountBySourceResolutionGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountBySourceResolutionGraph = _state.api.history.getPlaysBySourceResolution(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
Future<TautulliGraphData> _playCountByStreamResolutionGraph;
Future<TautulliGraphData> get playCountByStreamResolutionGraph => _playCountByStreamResolutionGraph;
set playCountByStreamResolutionGraph(Future<TautulliGraphData> playCountByStreamResolutionGraph) {
assert(playCountByStreamResolutionGraph != null);
_playCountByStreamResolutionGraph = playCountByStreamResolutionGraph;
notifyListeners();
}
void resetPlayCountByStreamResolutionGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByStreamResolutionGraph = _state.api.history.getPlaysByStreamResolution(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
Future<TautulliGraphData> _playCountByPlatformStreamTypeGraph;
Future<TautulliGraphData> get playCountByPlatformStreamTypeGraph => _playCountByPlatformStreamTypeGraph;
set playCountByPlatformStreamTypeGraph(Future<TautulliGraphData> playCountByPlatformStreamTypeGraph) {
assert(playCountByPlatformStreamTypeGraph != null);
_playCountByPlatformStreamTypeGraph = playCountByPlatformStreamTypeGraph;
notifyListeners();
}
void resetPlayCountByPlatformStreamTypeGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByPlatformStreamTypeGraph = _state.api.history.getStreamTypeByTopTenPlatforms(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
Future<TautulliGraphData> _playCountByUserStreamTypeGraph;
Future<TautulliGraphData> get playCountByUserStreamTypeGraph => _playCountByUserStreamTypeGraph;
set playCountByUserStreamTypeGraph(Future<TautulliGraphData> playCountByUserStreamTypeGraph) {
assert(playCountByUserStreamTypeGraph != null);
_playCountByUserStreamTypeGraph = playCountByUserStreamTypeGraph;
notifyListeners();
}
void resetPlayCountByUserStreamTypeGraph(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _playCountByUserStreamTypeGraph = _state.api.history.getStreamTypeByTopTenUsers(
timeRange: TautulliDatabaseValue.GRAPHS_DAYS.data,
yAxis: _state.graphYAxis,
);
notifyListeners();
}
void resetAllStreamInformationGraphs(BuildContext context) {
resetDailyStreamTypeBreakdownGraph(context);
resetPlayCountBySourceResolutionGraph(context);
resetPlayCountByStreamResolutionGraph(context);
resetPlayCountByPlatformStreamTypeGraph(context);
resetPlayCountByUserStreamTypeGraph(context);
}
///////////////
/// UPDATES ///
///////////////
Future<TautulliPMSUpdate> _updatePlexMediaServer;
Future<TautulliPMSUpdate> get updatePlexMediaServer => _updatePlexMediaServer;
set updatePlexMediaServer(Future<TautulliPMSUpdate> updatePlexMediaServer) {
assert(updatePlexMediaServer != null);
_updatePlexMediaServer = updatePlexMediaServer;
notifyListeners();
}
void resetUpdatePlexMediaServer(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _updatePlexMediaServer = _state.api.system.getPMSUpdate();
notifyListeners();
}
Future<TautulliUpdateCheck> _updateTautulli;
Future<TautulliUpdateCheck> get updateTautulli => _updateTautulli;
set updateTautulli(Future<TautulliUpdateCheck> updateTautulli) {
assert(updateTautulli != null);
_updateTautulli = updateTautulli;
notifyListeners();
}
void resetUpdateTautulli(BuildContext context) {
TautulliState _state = Provider.of<TautulliState>(context, listen: false);
if(_state.api != null) _updateTautulli = _state.api.system.updateCheck();
notifyListeners();
}
void resetAllUpdates(BuildContext context) {
resetUpdatePlexMediaServer(context);
resetUpdateTautulli(context);
}
}

View File

@@ -47,7 +47,12 @@ class _State extends State<TautulliActivityDetailsRoute> {
body: _body,
);
Widget get _appBar => LSAppBar(title: 'Activity Details');
Widget get _appBar => LSAppBar(
title: 'Activity Details',
actions: [
TautulliActivityDetailsMetadata(sessionId: widget.sessionId),
]
);
Widget get _body => LSRefreshIndicator(
refreshKey: _refreshKey,

View File

@@ -1,11 +1,33 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliActivityDetailsMetadata extends StatelessWidget {
final String sessionId;
TautulliActivityDetailsMetadata({
Key key,
@required this.sessionId,
}): super(key: key);
@override
Widget build(BuildContext context) => LSIconButton(
icon: Icons.info_outline,
onPressed: () async => _onPressed(context),
Widget build(BuildContext context) => Selector<TautulliState, Future<TautulliActivity>>(
selector: (_, state) => state.activity,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliActivity> snapshot) {
if(snapshot.hasError) return Container();
if(snapshot.hasData) {
TautulliSession session = snapshot.data.sessions.firstWhere((element) => element.sessionId == sessionId, orElse: () => null);
if(session != null) return LSIconButton(
icon: Icons.info_outline,
onPressed: () async => _onPressed(context),
);
}
return Container();
},
),
);
Future<void> _onPressed(BuildContext context) => LSSnackBar(

View File

@@ -0,0 +1,98 @@
import 'package:fluro_fork/fluro_fork.dart';
import 'package:flutter/material.dart' hide Router;
import 'package:flutter/scheduler.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliCheckForUpdatesRoute extends StatefulWidget {
static const ROUTE_NAME = '/tautulli/checkforupdates/:profile';
static String route({ String profile }) {
if(profile == null) return '/tautulli/checkforupdates/${LunaSeaDatabaseValue.ENABLED_PROFILE.data}';
return '/tautulli/checkforupdates/$profile';
}
static void defineRoute(Router router) => router.define(
ROUTE_NAME,
handler: Handler(handlerFunc: (context, params) => TautulliCheckForUpdatesRoute()),
transitionType: LunaRouter.transitionType,
);
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<TautulliCheckForUpdatesRoute> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
// Tracks the initial load to ensure the futures have been initialized
bool _initialLoad = false;
Future<void> _refresh() async {
TautulliLocalState _local = Provider.of<TautulliLocalState>(context, listen: false);
_local.resetAllUpdates(context);
setState(() => _initialLoad = true);
await Future.wait([
_local.updatePlexMediaServer,
_local.updateTautulli,
]);
}
@override
void initState() {
super.initState();
SchedulerBinding.instance.scheduleFrameCallback((_) => _refresh());
}
@override
Widget build(BuildContext context) => Scaffold(
key: _scaffoldKey,
appBar: _appBar,
body: _initialLoad ? _body : LSLoader(),
);
Widget get _appBar => LSAppBar(title: 'Check for Updates');
Widget get _body => LSRefreshIndicator(
refreshKey: _refreshKey,
onRefresh: _refresh,
child: FutureBuilder(
future: Future.wait([
Provider.of<TautulliLocalState>(context).updatePlexMediaServer,
Provider.of<TautulliLocalState>(context).updateTautulli,
]),
builder: (context, AsyncSnapshot<List<Object>> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliCheckForUpdatesRoute',
'_body',
'Unable to fetch updates',
snapshot.error,
null,
uploadToSentry: !(snapshot.error is DioError),
);
}
return LSErrorMessage(onTapHandler: () async => _refreshKey.currentState.show());
}
if(snapshot.hasData) return _list(
pms: snapshot.data[0],
tautulli: snapshot.data[1],
);
return LSLoader();
},
),
);
Widget _list({
@required TautulliPMSUpdate pms,
@required TautulliUpdateCheck tautulli,
}) => LSListView(
children: [
TautulliCheckForUpdatesPMSTile(update: pms),
TautulliCheckForUpdatesTautulliTile(update: tautulli),
],
);
}

View File

@@ -0,0 +1,2 @@
export 'widgets/pms_tile.dart';
export 'widgets/tautulli_tile.dart';

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:tautulli/tautulli.dart';
class TautulliCheckForUpdatesPMSTile extends StatelessWidget {
final TautulliPMSUpdate update;
TautulliCheckForUpdatesPMSTile({
Key key,
@required this.update,
}) : super(key: key);
@override
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: 'Plex Media Server'),
subtitle: _subtitle,
trailing: _trailing,
padContent: true,
);
Widget get _trailing => Column(
children: [
LSIconButton(
icon: CustomIcons.plex,
color: LSColors.list(0),
),
],
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
);
Widget get _subtitle => RichText(
text: TextSpan(
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
children: <TextSpan>[
if(!update.updateAvailable) TextSpan(
text: 'No Updates Available\n',
style: TextStyle(
color: LSColors.accent,
fontWeight: FontWeight.w600,
),
),
if(!update.updateAvailable) TextSpan(text: 'Current Version: ${update.version}'),
if(update.updateAvailable) TextSpan(
text: 'Update Available\n',
style: TextStyle(
color: LSColors.orange,
fontWeight: FontWeight.w600,
),
),
if(update.updateAvailable) TextSpan(text: 'Latest Version: ${update.version}'),
],
),
overflow: TextOverflow.fade,
maxLines: 3,
);
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:tautulli/tautulli.dart';
class TautulliCheckForUpdatesTautulliTile extends StatelessWidget {
final TautulliUpdateCheck update;
TautulliCheckForUpdatesTautulliTile({
Key key,
@required this.update,
}) : super(key: key);
@override
Widget build(BuildContext context) => LSCardTile(
title: LSTitle(text: 'Tautulli'),
subtitle: _subtitle,
trailing: _trailing,
padContent: true,
);
Widget get _trailing => Column(
children: [
LSIconButton(
icon: CustomIcons.tautulli,
color: LSColors.list(1),
),
],
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
);
Widget get _subtitle => RichText(
text: TextSpan(
style: TextStyle(
color: Colors.white70,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
children: <TextSpan>[
if(!update.update) TextSpan(
text: 'No Updates Available\n',
style: TextStyle(
color: LSColors.accent,
fontWeight: FontWeight.w600,
),
),
if(update.update) TextSpan(
text: 'Update Available\n',
style: TextStyle(
color: LSColors.orange,
fontWeight: FontWeight.w600,
),
),
if(update.update) TextSpan(
text: 'Current Version: ${update.currentRelease?? update.currentVersion?.substring(0, 7) ?? 'Unknown'}\n',
),
if(update.update) TextSpan(
text: 'Latest Version: ${update.latestRelease?? update.latestVersion?.substring(0, 7) ?? 'Unknown'}\n',
),
TextSpan(text: 'Install Type: ${update.installType}'),
],
),
overflow: TextOverflow.fade,
maxLines: 4,
);
}

View File

@@ -15,7 +15,7 @@ class TautulliGraphsTypeButton extends StatelessWidget {
onSelected: (value) {
Provider.of<TautulliState>(context, listen: false).graphYAxis = value;
Provider.of<TautulliLocalState>(context, listen: false).resetAllPlayPeriodGraphs(context);
Provider.of<TautulliLocalState>(context, listen: false).resetAllStreamInfoGraphs(context);
Provider.of<TautulliLocalState>(context, listen: false).resetAllStreamInformationGraphs(context);
},
itemBuilder: (context) => List<PopupMenuEntry<TautulliGraphYAxis>>.generate(
TautulliStatsType.values.length,

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayByPeriodRoute extends StatefulWidget {
TautulliGraphsPlayByPeriodRoute({
@@ -16,7 +15,6 @@ class TautulliGraphsPlayByPeriodRoute extends StatefulWidget {
class _State extends State<TautulliGraphsPlayByPeriodRoute> with AutomaticKeepAliveClientMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshKey = GlobalKey<RefreshIndicatorState>();
final String _placeholder = '<<!title!>>';
@override
bool get wantKeepAlive => true;
@@ -31,7 +29,11 @@ class _State extends State<TautulliGraphsPlayByPeriodRoute> with AutomaticKeepAl
TautulliLocalState _state = Provider.of<TautulliLocalState>(context, listen: false);
_state.resetAllPlayPeriodGraphs(context);
await Future.wait([
_state.playCountByDateGraph,
_state.dailyPlayCountGraph,
_state.playsByMonthGraph,
_state.playCountByDayOfWeekGraph,
_state.playCountByTopPlatformsGraph,
_state.playCountByTopUsersGraph,
]);
}
@@ -49,53 +51,52 @@ class _State extends State<TautulliGraphsPlayByPeriodRoute> with AutomaticKeepAl
onRefresh: _refresh,
child: LSListView(
children: [
..._dailyPlayCount,
..._playsByMonth,
//..._playCountByDayOfWeek,
//..._playCountByTopPlatforms,
//..._playCountByTopUsers,
LSHeader(
text: 'Daily',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days',
'\n\n',
'The total play count or duration of television, movies, and music played per day.'
].join(),
),
TautulliGraphsDailyPlayCountGraph(),
LSHeader(
text: 'Monthly',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_MONTHS.data} Months',
'\n\n',
'The combined total of television, movies, and music by month.',
].join(),
),
TautulliGraphsPlaysByMonthGraph(),
LSHeader(
text: 'By Day Of Week',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television, movies, and music played per day of the week.',
].join(),
),
TautulliGraphsPlayCountByDayOfWeekGraph(),
LSHeader(
text: 'By Top Platforms',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television, movies, and music played by the top most active platforms.',
].join(),
),
TautulliGraphsPlayCountByTopPlatformsGraph(),
LSHeader(
text: 'By Top Users',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television, movies, and music played by the top most active users.',
].join(),
),
TautulliGraphsPlayCountByTopUsersGraph(),
],
),
);
List<Widget> get _dailyPlayCount => [
LSHeader(
text: _createTitle('Daily Play $_placeholder'),
subtitle: 'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days',
),
TautulliGraphsPlayCountByDateGraph(),
];
List<Widget> get _playsByMonth => [
LSHeader(
text: 'Plays By Month',
subtitle: 'Last ${TautulliDatabaseValue.GRAPHS_MONTHS.data} Months',
),
TautulliGraphsPlaysByMonthGraph(),
];
List<Widget> get _playCountByDayOfWeek => [
LSHeader(
text: _createTitle('Play $_placeholder By Day Of Week'),
subtitle: 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
),
];
List<Widget> get _playCountByTopPlatforms => [
LSHeader(
text: _createTitle('Play $_placeholder By Top Platforms'),
subtitle: 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
),
];
List<Widget> get _playCountByTopUsers => [
LSHeader(
text: _createTitle('Play $_placeholder By Top Users'),
subtitle: 'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
),
];
String _createTitle(String title) => Provider.of<TautulliState>(context).graphYAxis == TautulliGraphYAxis.PLAYS
? title.replaceFirst(_placeholder, 'Count')
: title.replaceFirst(_placeholder, 'Duration');
}
}

View File

@@ -1,2 +1,5 @@
export 'widgets/play_count_by_date_graph.dart';
export 'widgets/daily_play_count_graph.dart';
export 'widgets/play_count_by_day_of_week_graph.dart';
export 'widgets/play_count_by_top_platforms_graph.dart';
export 'widgets/play_count_by_top_users_graph.dart';
export 'widgets/plays_by_month_graph.dart';

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsDailyPlayCountGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.dailyPlayCountGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsDailyPlayCountGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByDate',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: LineChart(
LineChartData(
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliLineGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
lineBarsData: TautulliLineGraphHelper.lineBarsData(data),
lineTouchData: TautulliLineGraphHelper.lineTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -1,160 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByDateGraph extends StatelessWidget {
static const double _height = 250.0;
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByDateGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByDateGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByDate',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return _error;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return _loading;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Container(
height: _height,
child: Padding(
child: LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(
leftTitles: SideTitles(showTitles: false),
rightTitles: SideTitles(showTitles: false),
topTitles: SideTitles(showTitles: false),
bottomTitles: SideTitles(
showTitles: true,
margin: 26.0,
rotateAngle: -90.0,
getTitles: (value) => DateTime.tryParse((data.categories[value.truncate()])) != null
? DateFormat('MM/dd').format(DateTime.parse((data.categories[value.truncate()])))?.toString()
: '??/??',
textStyle: TextStyle(
color: Colors.white30,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.white12),
),
lineBarsData: List<LineChartBarData>.generate(
data.series.length,
(sIndex) => LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
barWidth: 3.0,
colors: [LSColors.list(sIndex)],
spots: List<FlSpot>.generate(
data.series[sIndex].data.length,
(dIndex) => FlSpot(dIndex.toDouble(), data.series[sIndex].data[dIndex].toDouble()),
),
belowBarData: BarAreaData(
show: true,
colors: [LSColors.list(sIndex).withOpacity(0.20)],
),
dotData: FlDotData(
show: true,
getDotPainter: (FlSpot spot, double xPercentage, LineChartBarData bar, int index) => FlDotCirclePainter(
radius: 2.50,
strokeColor: bar.colors[0],
color: bar.colors[0],
),
),
),
),
lineTouchData: LineTouchData(
enabled: true,
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: LunaSeaDatabaseValue.THEME_AMOLED.data ? Colors.black : LSColors.primary,
tooltipRoundedRadius: Constants.UI_BORDER_RADIUS,
tooltipPadding: EdgeInsets.all(8.0),
maxContentWidth: MediaQuery.of(context).size.width/1.25,
fitInsideVertically: true,
fitInsideHorizontally: true,
getTooltipItems: (List<LineBarSpot> spots) => List<LineTooltipItem>.generate(
spots.length,
(index) => LineTooltipItem(
[
'${data.series[spots[index].barIndex].name}: ',
Provider.of<TautulliState>(context, listen: false).graphYAxis == TautulliGraphYAxis.PLAYS
? '${spots[index]?.y?.truncate() ?? 0}'
: '${Duration(seconds: spots[index]?.y?.truncate() ?? 0).lsDuration_fullTimestamp()}',
].join().trim(),
TextStyle(
color: LSColors.list(index),
fontWeight: FontWeight.w600,
),
),
),
),
getTouchedSpotIndicator: (bar, data) => List<TouchedSpotIndicatorData>.generate(
data.length,
(index) => TouchedSpotIndicatorData(
FlLine(
strokeWidth: 3.0,
color: bar.colors[0].withOpacity(0.50),
),
FlDotData(
show: true,
getDotPainter: (FlSpot spot, double xPercentage, LineChartBarData bar, int index) => FlDotCirclePainter(
radius: 5.0,
strokeColor: bar.colors[0],
color: bar.colors[0],
),
),
),
),
),
),
),
padding: EdgeInsets.fromLTRB(14.0, 14.0, 14.0, 2.0),
),
),
);
}
Widget get _loading => LSCard(
child: Container(
height: _height,
child: LSLoader(),
),
);
Widget get _error => LSCard(
child: Container(
height: _height,
alignment: Alignment.center,
child: LSIconButton(
icon: Icons.error,
iconSize: 60.0,
color: Colors.white12,
),
),
);
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByDayOfWeekGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByDayOfWeekGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByDayOfWeekGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByDayOfWeek',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data, maxTitleLength: 3, titleOverFlowShowEllipsis: false),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByTopPlatformsGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByTopPlatformsGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByTopPlatformsGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByTopTenPlatforms',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByTopUsersGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByTopUsersGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByTopUsersGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByTopTenUsers',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -5,8 +5,6 @@ import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlaysByMonthGraph extends StatelessWidget {
static const double _height = 250.0;
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playsByMonthGraph,
@@ -24,104 +22,38 @@ class TautulliGraphsPlaysByMonthGraph extends StatelessWidget {
uploadToSentry: !(snapshot.error is DioError),
);
}
return _error;
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return _loading;
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Container(
height: _height,
child: Padding(
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceEvenly,
gridData: FlGridData(show: false),
titlesData: FlTitlesData(
leftTitles: SideTitles(showTitles: false),
rightTitles: SideTitles(showTitles: false),
topTitles: SideTitles(showTitles: false),
bottomTitles: SideTitles(
showTitles: true,
reservedSize: 8.0,
getTitles: (value) => data.categories[value.truncate()].substring(0, 3).toUpperCase(),
textStyle: TextStyle(
color: Colors.white30,
fontSize: Constants.UI_FONT_SIZE_SUBTITLE,
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.white12),
),
barGroups: List<BarChartGroupData>.generate(
data.categories.length,
(cIndex) => BarChartGroupData(
x: cIndex,
barRods: List<BarChartRodData>.generate(
data.series.length,
(sIndex) => BarChartRodData(
width: 11.0,
y: data.series[sIndex].data[cIndex].toDouble(),
borderRadius: BorderRadius.circular(Constants.UI_BORDER_RADIUS),
color: LSColors.list(sIndex),
),
),
),
),
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: LunaSeaDatabaseValue.THEME_AMOLED.data ? Colors.black : LSColors.primary,
tooltipRoundedRadius: Constants.UI_BORDER_RADIUS,
tooltipPadding: EdgeInsets.all(8.0),
maxContentWidth: MediaQuery.of(context).size.width/1.25,
fitInsideVertically: true,
fitInsideHorizontally: true,
getTooltipItem: (group, gIndex, rod, rIndex) => BarTooltipItem(
[
'${data.series[rIndex].name}: ',
Provider.of<TautulliState>(context, listen: false).graphYAxis == TautulliGraphYAxis.PLAYS
? '${data.series[rIndex].data[gIndex] ?? 0}'
: '${Duration(seconds: data.series[rIndex].data[gIndex] ?? 0).lsDuration_fullTimestamp()}',
].join().trim(),
TextStyle(
color: LSColors.list(rIndex),
fontWeight: FontWeight.w600,
),
),
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data, maxTitleLength: 3, titleOverFlowShowEllipsis: false),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
padding: EdgeInsets.all(14.0),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
Widget get _loading => LSCard(
child: Container(
height: _height,
child: LSLoader(),
),
);
Widget get _error => LSCard(
child: Container(
height: _height,
alignment: Alignment.center,
child: LSIconButton(
icon: Icons.error,
iconSize: 60.0,
color: Colors.white12,
),
),
);
}

View File

@@ -27,8 +27,14 @@ class _State extends State<TautulliGraphsStreamInformationRoute> with AutomaticK
Future<void> _refresh() async {
TautulliLocalState _state = Provider.of<TautulliLocalState>(context, listen: false);
_state.resetAllStreamInfoGraphs(context);
await Future.wait([]);
_state.resetAllStreamInformationGraphs(context);
await Future.wait([
_state.dailyStreamTypeBreakdownGraph,
_state.playCountBySourceResolutionGraph,
_state.playCountByStreamResolutionGraph,
_state.playCountByPlatformStreamTypeGraph,
_state.playCountByUserStreamTypeGraph,
]);
}
@override
@@ -36,7 +42,7 @@ class _State extends State<TautulliGraphsStreamInformationRoute> with AutomaticK
super.build(context);
return Scaffold(
key: _scaffoldKey,
body: _comingSoon,
body: _body,
);
}
@@ -44,9 +50,53 @@ class _State extends State<TautulliGraphsStreamInformationRoute> with AutomaticK
refreshKey: _refreshKey,
onRefresh: _refresh,
child: LSListView(
children: [],
children: [
LSHeader(
text: 'Daily Stream Type Breakdown',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_LINECHART_DAYS.data} Days',
'\n\n',
'The total play count or duration of television, movies, and music by the transcode decision.',
].join(),
),
TautulliGraphsDailyStreamTypeBreakdownGraph(),
LSHeader(
text: 'By Source Resolution',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television and movies by their original resolution (pre-transcoding).',
].join(),
),
TautulliGraphsPlayCountBySourceResolutionGraph(),
LSHeader(
text: 'By Stream Resolution',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television and movies by their original resolution (pre-transcoding).',
].join(),
),
TautulliGraphsPlayCountByStreamResolutionGraph(),
LSHeader(
text: 'By Platform Stream Type',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television and movies by their original resolution (pre-transcoding).',
].join(),
),
TautulliGraphsPlayCountByPlatformStreamTypeGraph(),
LSHeader(
text: 'By User Stream Type',
subtitle: [
'Last ${TautulliDatabaseValue.GRAPHS_DAYS.data} Days',
'\n\n',
'The combined total of television and movies by their original resolution (pre-transcoding).',
].join(),
),
TautulliGraphsPlayCountByUserStreamTypeGraph(),
],
),
);
Widget get _comingSoon => LSGenericMessage(text: 'Coming Soon');
}

View File

@@ -0,0 +1,5 @@
export 'widgets/daily_stream_type_breakdown_graph.dart';
export 'widgets/play_count_by_platform_stream_type_graph.dart';
export 'widgets/play_count_by_source_resolution_graph.dart';
export 'widgets/play_count_by_stream_resolution_graph.dart';
export 'widgets/play_count_by_user_stream_type_graph.dart';

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsDailyStreamTypeBreakdownGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.dailyStreamTypeBreakdownGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsDailyStreamTypeBreakdownGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByDate',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: LineChart(
LineChartData(
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliLineGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
lineBarsData: TautulliLineGraphHelper.lineBarsData(data),
lineTouchData: TautulliLineGraphHelper.lineTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByPlatformStreamTypeGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByPlatformStreamTypeGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByPlatformStreamTypeGraph',
'_body',
'Unable to fetch Tautulli graph data: getStreamTypeByTopTenPlatforms',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountBySourceResolutionGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountBySourceResolutionGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountBySourceResolutionGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysBySourceResolution',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByStreamResolutionGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByStreamResolutionGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByStreamResolutionGraph',
'_body',
'Unable to fetch Tautulli graph data: getPlaysByStreamResolution',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:tautulli/tautulli.dart';
class TautulliGraphsPlayCountByUserStreamTypeGraph extends StatelessWidget {
@override
Widget build(BuildContext context) => Selector<TautulliLocalState, Future<TautulliGraphData>>(
selector: (_, state) => state.playCountByUserStreamTypeGraph,
builder: (context, future, _) => FutureBuilder(
future: future,
builder: (context, AsyncSnapshot<TautulliGraphData> snapshot) {
if(snapshot.hasError) {
if(snapshot.connectionState != ConnectionState.waiting) {
Logger.error(
'TautulliGraphsPlayCountByUserStreamTypeGraph',
'_body',
'Unable to fetch Tautulli graph data: getStreamTypeByTopTenUsers',
snapshot.error,
StackTrace.current,
uploadToSentry: !(snapshot.error is DioError),
);
}
return TautulliGraphHelper.errorContainer;
}
if(snapshot.hasData) return _graph(context, snapshot.data);
return TautulliGraphHelper.loadingContainer;
},
),
);
Widget _graph(BuildContext context, TautulliGraphData data) {
return LSCard(
child: Column(
children: [
Container(
height: TautulliGraphHelper.GRAPH_HEIGHT,
width: MediaQuery.of(context).size.width,
child: Padding(
child: BarChart(
BarChartData(
alignment: TautulliGraphHelper.chartAlignment(),
gridData: TautulliGraphHelper.gridData(),
titlesData: TautulliGraphHelper.titlesData(data),
borderData: TautulliGraphHelper.borderData(),
barGroups: TautulliBarGraphHelper.barGroups(context, data),
barTouchData: TautulliBarGraphHelper.barTouchData(context, data),
),
),
padding: EdgeInsets.all(14.0),
),
),
TautulliGraphHelper.createLegend(data.series),
],
),
);
}
}

View File

@@ -101,7 +101,10 @@ class _State extends State<TautulliHistoryDetailsRoute> {
body: _body,
);
Widget get _appBar => LSAppBar(title: 'History Details');
Widget get _appBar => LSAppBar(
title: 'History Details',
actions: [],
);
Widget get _body => LSRefreshIndicator(
refreshKey: _refreshKey,

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/tautulli.dart';
class TautulliMoreCheckForUpdatesTile extends StatelessWidget {
@override
@@ -13,10 +14,8 @@ class TautulliMoreCheckForUpdatesTile extends StatelessWidget {
onTap: () async => _onTap(context),
);
Future<void> _onTap(BuildContext context) async => LSSnackBar(
context: context,
title: 'Coming Soon!',
message: 'Feature has not yet been implemented',
type: SNACKBAR_TYPE.info,
Future<void> _onTap(BuildContext context) async => TautulliRouter.router.navigateTo(
context,
TautulliCheckForUpdatesRoute.route(),
);
}

View File

@@ -14,15 +14,8 @@ class TautulliMoreGraphsTile extends StatelessWidget {
onTap: () async => _onTap(context),
);
Future<void> _onTap(BuildContext context) async => LSSnackBar(
context: context,
title: 'Coming Soon!',
message: 'Feature has not yet been implemented',
type: SNACKBAR_TYPE.info,
Future<void> _onTap(BuildContext context) async => TautulliRouter.router.navigateTo(
context,
TautulliGraphsRoute.route(),
);
// Future<void> _onTap(BuildContext context) async => TautulliRouter.router.navigateTo(
// context,
// TautulliGraphsRoute.route(),
// );
}

View File

@@ -128,7 +128,7 @@ class TautulliRecentlyAddedContentTile extends StatelessWidget {
Future<void> _onTap(BuildContext context) async => LSSnackBar(
context: context,
title: 'Coming Soon!',
message: 'This feature has not yet been implemented',
message: 'Library data has not yet been implemented',
type: SNACKBAR_TYPE.info,
);
}

View File

@@ -117,7 +117,7 @@ class TautulliStatisticsMediaTile extends StatelessWidget {
Future<void> _onTap(BuildContext context) async => LSSnackBar(
context: context,
title: 'Coming Soon!',
message: 'This feature has not yet been implemented',
message: 'Library data has not yet been implemented',
type: SNACKBAR_TYPE.info,
);
}

View File

@@ -96,7 +96,7 @@ class TautulliStatisticsRecentlyWatchedTile extends StatelessWidget {
Future<void> _onTap(BuildContext context) async => LSSnackBar(
context: context,
title: 'Coming Soon!',
message: 'This feature has not yet been implemented',
message: 'Library data has not yet been implemented',
type: SNACKBAR_TYPE.info,
);
}

View File

@@ -27,7 +27,7 @@ class TautulliGlobalSettings extends StatelessWidget {
Provider.of<TautulliState>(context, listen: false).api.system.backupConfig()
.then((_) => LSSnackBar(
context: context,
title: 'Backing Up Configuration...',
title: 'Backing Up Configuration${Constants.TEXT_ELLIPSIS}',
message: 'Backing up your configuration in the background',
))
.catchError((error, trace) {
@@ -51,7 +51,7 @@ class TautulliGlobalSettings extends StatelessWidget {
Provider.of<TautulliState>(context, listen: false).api.system.backupDB()
.then((_) => LSSnackBar(
context: context,
title: 'Backing Up Database...',
title: 'Backing Up Database${Constants.TEXT_ELLIPSIS}',
message: 'Backing up your database in the background',
))
.catchError((error, trace) {
@@ -75,7 +75,7 @@ class TautulliGlobalSettings extends StatelessWidget {
Provider.of<TautulliState>(context, listen: false).api.system.deleteCache()
.then((_) => LSSnackBar(
context: context,
title: 'Deleting Cache...',
title: 'Deleting Cache${Constants.TEXT_ELLIPSIS}',
message: 'Tautulli cache is being deleted',
))
.catchError((error, trace) {
@@ -99,7 +99,7 @@ class TautulliGlobalSettings extends StatelessWidget {
Provider.of<TautulliState>(context, listen: false).api.system.deleteImageCache()
.then((_) => LSSnackBar(
context: context,
title: 'Deleting Image Cache...',
title: 'Deleting Image Cache${Constants.TEXT_ELLIPSIS}',
message: 'Tautulli image cache is being deleted',
))
.catchError((error, trace) {

View File

@@ -465,7 +465,7 @@ packages:
name: in_app_purchase
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4+5"
version: "0.3.4+6"
intl:
dependency: "direct main"
description:
@@ -827,7 +827,7 @@ packages:
name: tautulli
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
@@ -869,7 +869,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.5.2"
version: "5.5.3"
url_launcher_linux:
dependency: transitive
description:

View File

@@ -1,6 +1,6 @@
name: lunasea
description: Self Hosted Manager
version: 4.0.0+400009
version: 4.0.0+400010
publish_to: 'none'
environment:
sdk: ">=2.7.0 <3.0.0"
@@ -26,7 +26,7 @@ dependencies:
google_nav_bar: ^3.0.0
hive: ^1.4.4
hive_flutter: ^0.3.1
in_app_purchase: ^0.3.4+5
in_app_purchase: ^0.3.4+6
intl: ^0.16.1
package_info: ^0.4.3
path_provider: ^1.6.14
@@ -38,11 +38,11 @@ dependencies:
stack_trace: ^1.9.5
table_calendar: ^2.2.3
tuple: ^1.0.3
url_launcher: ^5.5.2
url_launcher: ^5.5.3
uuid: ^2.2.2
xml_parser: ^0.1.2
# LunaTools Packages
tautulli: ^1.0.5
tautulli: ^1.1.0
wake_on_lan: ^1.1.1+1
dev_dependencies: