From 404647c4cb849f82f9db8d29757d2a661a64f813 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Mon, 3 Sep 2018 03:00:54 +0100 Subject: [PATCH] add storage stats card to My LBRY downloads page (#278) * add storage stats card to My LBRY downloads page * add decimalPoints parameter to formatBytes method * some more tweaks to notifications and startup * cancel all notifications if the service is not running when the activity is destroyed --- app/src/component/storageStatsCard/index.js | 4 + app/src/component/storageStatsCard/view.js | 128 ++++++++++++++++++ app/src/page/downloads/view.js | 54 +++++--- app/src/page/splash/view.js | 2 +- app/src/styles/colors.js | 6 +- app/src/styles/downloads.js | 13 +- app/src/styles/storageStats.js | 84 ++++++++++++ app/src/utils/helper.js | 81 ++++++----- .../java/io/lbry/browser/LbrynetService.java | 2 +- .../java/io/lbry/browser/MainActivity.java | 6 + 10 files changed, 321 insertions(+), 59 deletions(-) create mode 100644 app/src/component/storageStatsCard/index.js create mode 100644 app/src/component/storageStatsCard/view.js create mode 100644 app/src/styles/storageStats.js diff --git a/app/src/component/storageStatsCard/index.js b/app/src/component/storageStatsCard/index.js new file mode 100644 index 0000000..22a0472 --- /dev/null +++ b/app/src/component/storageStatsCard/index.js @@ -0,0 +1,4 @@ +import { connect } from 'react-redux'; +import StorageStatsCard from './view'; + +export default connect()(StorageStatsCard); diff --git a/app/src/component/storageStatsCard/view.js b/app/src/component/storageStatsCard/view.js new file mode 100644 index 0000000..c21722e --- /dev/null +++ b/app/src/component/storageStatsCard/view.js @@ -0,0 +1,128 @@ +import React from 'react'; +import { normalizeURI, parseURI } from 'lbry-redux'; +import { + ActivityIndicator, + Platform, + Switch, + Text, + TouchableOpacity, + View +} from 'react-native'; +import { formatBytes } from '../../utils/helper'; +import Colors from '../../styles/colors'; +import storageStatsStyle from '../../styles/storageStats'; + +class StorageStatsCard extends React.PureComponent { + state = { + totalBytes: 0, + totalAudioBytes: 0, + totalAudioPercent: 0, + totalImageBytes: 0, + totalImagePercent: 0, + totalVideoBytes: 0, + totalVideoPercent: 0, + totalOtherBytes: 0, + totalOtherPercent: 0, + showStats: false + }; + + componentDidMount() { + // calculate total bytes + const { fileInfos } = this.props; + + let totalBytes = 0, totalAudioBytes = 0, totalImageBytes = 0, totalVideoBytes = 0; + let totalAudioPercent = 0, totalImagePercent = 0, totalVideoPercent = 0; + + fileInfos.forEach(fileInfo => { + if (fileInfo.completed) { + const bytes = fileInfo.written_bytes; + const type = fileInfo.mime_type; + totalBytes += bytes; + if (type) { + if (type.startsWith('audio/')) totalAudioBytes += bytes; + if (type.startsWith('image/')) totalImageBytes += bytes; + if (type.startsWith('video/')) totalVideoBytes += bytes; + } + } + }); + + totalAudioPercent = ((totalAudioBytes / totalBytes) * 100).toFixed(2); + totalImagePercent = ((totalImageBytes / totalBytes) * 100).toFixed(2); + totalVideoPercent = ((totalVideoBytes / totalBytes) * 100).toFixed(2); + + this.setState({ + totalBytes, + totalAudioBytes, + totalAudioPercent, + totalImageBytes, + totalImagePercent, + totalVideoBytes, + totalVideoPercent, + totalOtherBytes: totalBytes - (totalAudioBytes + totalImageBytes + totalVideoBytes), + totalOtherPercent: (100 - (parseFloat(totalAudioPercent) + + parseFloat(totalImagePercent) + + parseFloat(totalVideoPercent))).toFixed(2) + }); + } + + render() { + return ( + + + + {formatBytes(this.state.totalBytes, 2)} + used + + + Stats + this.setState({ showStats: value })} /> + + + {this.state.showStats && + + + + + + + + + {this.state.totalAudioBytes > 0 && + + + Audio + {formatBytes(this.state.totalAudioBytes, 2)} + + } + {this.state.totalImageBytes > 0 && + + + Images + {formatBytes(this.state.totalImageBytes, 2)} + + } + {this.state.totalVideoBytes > 0 && + + + Videos + {formatBytes(this.state.totalVideoBytes, 2)} + + } + {this.state.totalOtherBytes > 0 && + + + Other + {formatBytes(this.state.totalOtherBytes, 2)} + + } + + } + + ) + } +} + +export default StorageStatsCard; diff --git a/app/src/page/downloads/view.js b/app/src/page/downloads/view.js index f8a924d..96a7b66 100644 --- a/app/src/page/downloads/view.js +++ b/app/src/page/downloads/view.js @@ -14,6 +14,7 @@ import Colors from '../../styles/colors'; import PageHeader from '../../component/pageHeader'; import FileListItem from '../../component/fileListItem'; import FloatingWalletBalance from '../../component/floatingWalletBalance'; +import StorageStatsCard from '../../component/storageStatsCard'; import UriBar from '../../component/uriBar'; import downloadsStyle from '../../styles/downloads'; import fileListStyle from '../../styles/fileList'; @@ -41,29 +42,38 @@ class DownloadsPage extends React.PureComponent { return ( - {!fetching && !hasDownloads && You have not downloaded anything from LBRY yet.} - {fetching && !hasDownloads && } + {!fetching && !hasDownloads && + + You have not downloaded anything from LBRY yet. + } + {fetching && !hasDownloads && + + + } {hasDownloads && - ( - navigateToUri(navigation, this.uriFromFileInfo(item), { autoplay: true })} /> - ) - } - data={fileInfos.sort((a, b) => { - // TODO: Implement sort based on user selection - if (!a.completed && b.completed) return -1; - if (a.completed && !b.completed) return 1; - if (a.metadata.title === b.metadata.title) return 0; - return (a.metadata.title < b.metadata.title) ? -1 : 1; - })} - keyExtractor={(item, index) => item.outpoint} - />} + + + ( + navigateToUri(navigation, this.uriFromFileInfo(item), { autoplay: true })} /> + ) + } + data={fileInfos.sort((a, b) => { + // TODO: Implement sort based on user selection + if (!a.completed && b.completed) return -1; + if (a.completed && !b.completed) return 1; + if (a.metadata.title === b.metadata.title) return 0; + return (a.metadata.title < b.metadata.title) ? -1 : 1; + })} + keyExtractor={(item, index) => item.outpoint} + /> + } diff --git a/app/src/page/splash/view.js b/app/src/page/splash/view.js index 0359edd..dcd619b 100644 --- a/app/src/page/splash/view.js +++ b/app/src/page/splash/view.js @@ -121,7 +121,7 @@ class SplashScreen extends React.PureComponent { const { deleteCompleteBlobs } = this.props; const startupStatus = status.startup_status; // At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve - const hasStarted = startupStatus.wallet && status.wallet.blocks_behind <= 0; + const hasStarted = startupStatus.file_manager && startupStatus.wallet && status.wallet.blocks_behind <= 0; if (hasStarted) { deleteCompleteBlobs(); diff --git a/app/src/styles/colors.js b/app/src/styles/colors.js index 9b1c0b5..71b4c4e 100644 --- a/app/src/styles/colors.js +++ b/app/src/styles/colors.js @@ -10,7 +10,11 @@ const Colors = { Orange: '#ffbb00', Red: '#ff0000', VeryLightGrey: '#f1f1f1', - White: '#ffffff' + White: '#ffffff', + + StatsAudio: '#f6a637', + StatsImage: '#ff4a7d', + StatsOther: '#26bcf7' }; export default Colors; diff --git a/app/src/styles/downloads.js b/app/src/styles/downloads.js index ec04a9d..5c6bc4f 100644 --- a/app/src/styles/downloads.js +++ b/app/src/styles/downloads.js @@ -2,22 +2,29 @@ import { StyleSheet } from 'react-native'; const downloadsStyle = StyleSheet.create({ container: { + flex: 1 + }, + busyContainer: { flex: 1, justifyContent: 'center', - alignItems: 'center' + alignItems: 'center', + flexDirection: 'row' + }, + subContainer: { + flex: 1 }, itemList: { flex: 1, }, scrollContainer: { flex: 1, - width: '100%', - height: '100%', paddingLeft: 16, paddingRight: 16, + marginTop: 16, marginBottom: 60 }, scrollPadding: { + marginTop: -16, paddingBottom: 16 }, noDownloadsText: { diff --git a/app/src/styles/storageStats.js b/app/src/styles/storageStats.js new file mode 100644 index 0000000..c6d078f --- /dev/null +++ b/app/src/styles/storageStats.js @@ -0,0 +1,84 @@ +import { StyleSheet } from 'react-native'; +import Colors from './colors'; + +const storageStatsStyle = StyleSheet.create({ + container: { + flex: 1 + }, + row: { + flexDirection: 'row' + }, + card: { + backgroundColor: Colors.White, + marginTop: 16, + marginLeft: 16, + marginRight: 16, + padding: 16 + }, + totalSize: { + fontFamily: 'Metropolis-Regular', + fontSize: 36 + }, + annotation: { + fontFamily: 'Metropolis-Regular', + fontSize: 14, + marginTop: -4 + }, + statsText: { + fontFamily: 'Metropolis-Regular', + fontSize: 14 + }, + distributionBar: { + flexDirection: 'row', + width: '100%', + height: 8, + marginTop: 16, + marginBottom: 16 + }, + audioDistribution: { + backgroundColor: Colors.StatsAudio + }, + imageDistribution: { + backgroundColor: Colors.StatsImage + }, + videoDistribution: { + backgroundColor: Colors.LbryGreen + }, + otherDistribution: { + backgroundColor: Colors.StatsOther + }, + legendItem: { + alignItems: 'center', + marginBottom: 8, + justifyContent: 'space-between' + }, + legendBox: { + width: 16, + height: 16, + }, + legendText: { + fontFamily: 'Metropolis-Regular', + fontSize: 14, + flex: 0.3 + }, + legendSize: { + fontFamily: 'Metropolis-Regular', + fontSize: 14, + flex: 0.6, + textAlign: 'right' + }, + statsToggle: { + marginLeft: 8, + }, + summary: { + flex: 0.5, + alignSelf: 'flex-start' + }, + toggleStatsContainer: { + flex: 0.5, + alignItems: 'center', + justifyContent: 'flex-end' + } +}); + +export default storageStatsStyle; diff --git a/app/src/utils/helper.js b/app/src/utils/helper.js index 1a17789..2807e1a 100644 --- a/app/src/utils/helper.js +++ b/app/src/utils/helper.js @@ -1,40 +1,59 @@ import { NavigationActions, StackActions } from 'react-navigation'; -export function navigateToUri(navigation, uri, additionalParams) { - if (!navigation) { - return; - } - - if (uri === navigation.state.key) { - return; - } - - const params = Object.assign({ uri }, additionalParams); - if ('File' === navigation.state.routeName) { +export function dispatchNavigateToUri(dispatch, nav, uri) { + const params = { uri }; + if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) { + const mainRoute = nav.routes[0]; + const discoverRoute = mainRoute.routes[0]; + if (discoverRoute.index > 0 && 'File' === discoverRoute.routes[discoverRoute.index].routeName) { + const fileRoute = discoverRoute.routes[discoverRoute.index]; + // Currently on a file page, so we can ignore (if the URI is the same) or replace (different URIs) + if (uri !== fileRoute.params.uri) { const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params }); - navigation.dispatch(stackAction); + dispatch(stackAction); return; + } } + } - navigation.navigate({ routeName: 'File', key: uri, params }); + const navigateAction = NavigationActions.navigate({ routeName: 'File', key: uri, params }); + dispatch(navigateAction); } -export function dispatchNavigateToUri(dispatch, nav, uri) { - const params = { uri }; - if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) { - const mainRoute = nav.routes[0]; - const discoverRoute = mainRoute.routes[0]; - if (discoverRoute.index > 0 && 'File' === discoverRoute.routes[discoverRoute.index].routeName) { - const fileRoute = discoverRoute.routes[discoverRoute.index]; - // Currently on a file page, so we can ignore (if the URI is the same) or replace (different URIs) - if (uri !== fileRoute.params.uri) { - const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params }); - dispatch(stackAction); - return; - } - } - } +export function formatBytes(bytes, decimalPoints = 0) { + if (!bytes) { + return '0 KB'; + } - const navigateAction = NavigationActions.navigate({ routeName: 'File', key: uri, params }); - dispatch(navigateAction); -} \ No newline at end of file + if (bytes < 1048576) { // < 1MB + const value = (bytes / 1024.0).toFixed(decimalPoints); + return `${value} KB`; + } + + if (bytes < 1073741824) { // < 1GB + const value = (bytes / (1024.0 * 1024.0)).toFixed(decimalPoints); + return `${value} MB`; + } + + const value = (bytes / (1024.0 * 1024.0 * 1024.0)).toFixed(decimalPoints); + return `${value} GB`; +} + +export function navigateToUri(navigation, uri, additionalParams) { + if (!navigation) { + return; + } + + if (uri === navigation.state.key) { + return; + } + + const params = Object.assign({ uri }, additionalParams); + if ('File' === navigation.state.routeName) { + const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params }); + navigation.dispatch(stackAction); + return; + } + + navigation.navigate({ routeName: 'File', key: uri, params }); +} diff --git a/src/main/java/io/lbry/browser/LbrynetService.java b/src/main/java/io/lbry/browser/LbrynetService.java index eb7fec8..8384dc1 100644 --- a/src/main/java/io/lbry/browser/LbrynetService.java +++ b/src/main/java/io/lbry/browser/LbrynetService.java @@ -37,7 +37,7 @@ import org.renpy.android.ResourceManager; */ public class LbrynetService extends PythonService { - private static final int SERVICE_NOTIFICATION_GROUP_ID = -1; + public static final int SERVICE_NOTIFICATION_GROUP_ID = 5; public static final String ACTION_STOP_SERVICE = "io.lbry.browser.ACTION_STOP_SERVICE"; diff --git a/src/main/java/io/lbry/browser/MainActivity.java b/src/main/java/io/lbry/browser/MainActivity.java index fa84e2b..667c34e 100644 --- a/src/main/java/io/lbry/browser/MainActivity.java +++ b/src/main/java/io/lbry/browser/MainActivity.java @@ -83,6 +83,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand */ private boolean serviceRunning; + private boolean receivedStopService; + protected String getMainComponentName() { return "LBRYApp"; } @@ -138,6 +140,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand stopServiceReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + MainActivity.this.receivedStopService = true; MainActivity.this.finish(); } }; @@ -383,6 +386,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand notificationManager.cancel(downloadNotificationIds.get(i)); } } + if (receivedStopService || !isServiceRunning(LbrynetService.class)) { + notificationManager.cancelAll(); + } super.onDestroy(); if (mReactInstanceManager != null) {