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
This commit is contained in:
Akinwale Ariwodola 2018-09-03 03:00:54 +01:00 committed by GitHub
parent c0b464ae36
commit 404647c4cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 321 additions and 59 deletions

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import StorageStatsCard from './view';
export default connect()(StorageStatsCard);

View file

@ -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 (
<View style={storageStatsStyle.card}>
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>
<View style={storageStatsStyle.summary}>
<Text style={storageStatsStyle.totalSize}>{formatBytes(this.state.totalBytes, 2)}</Text>
<Text style={storageStatsStyle.annotation}>used</Text>
</View>
<View style={[storageStatsStyle.row, storageStatsStyle.toggleStatsContainer]}>
<Text style={storageStatsStyle.statsText}>Stats</Text>
<Switch
style={storageStatsStyle.statsToggle}
value={this.state.showStats}
onValueChange={(value) => this.setState({ showStats: value })} />
</View>
</View>
{this.state.showStats &&
<View>
<View style={storageStatsStyle.distributionBar}>
<View style={[storageStatsStyle.audioDistribution, { flex: parseFloat(this.state.totalAudioPercent) }]} />
<View style={[storageStatsStyle.imageDistribution, { flex: parseFloat(this.state.totalImagePercent) }]} />
<View style={[storageStatsStyle.videoDistribution, { flex: parseFloat(this.state.totalVideoPercent) }]} />
<View style={[storageStatsStyle.otherDistribution, { flex: parseFloat(this.state.totalOtherPercent) }]} />
</View>
<View style={storageStatsStyle.legend}>
{this.state.totalAudioBytes > 0 &&
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.audioDistribution]} />
<Text style={storageStatsStyle.legendText}>Audio</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalAudioBytes, 2)}</Text>
</View>
}
{this.state.totalImageBytes > 0 &&
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.imageDistribution]} />
<Text style={storageStatsStyle.legendText}>Images</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalImageBytes, 2)}</Text>
</View>
}
{this.state.totalVideoBytes > 0 &&
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.videoDistribution]} />
<Text style={storageStatsStyle.legendText}>Videos</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalVideoBytes, 2)}</Text>
</View>
}
{this.state.totalOtherBytes > 0 &&
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.otherDistribution]} />
<Text style={storageStatsStyle.legendText}>Other</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalOtherBytes, 2)}</Text>
</View>
}
</View>
</View>}
</View>
)
}
}
export default StorageStatsCard;

View file

@ -14,6 +14,7 @@ import Colors from '../../styles/colors';
import PageHeader from '../../component/pageHeader'; import PageHeader from '../../component/pageHeader';
import FileListItem from '../../component/fileListItem'; import FileListItem from '../../component/fileListItem';
import FloatingWalletBalance from '../../component/floatingWalletBalance'; import FloatingWalletBalance from '../../component/floatingWalletBalance';
import StorageStatsCard from '../../component/storageStatsCard';
import UriBar from '../../component/uriBar'; import UriBar from '../../component/uriBar';
import downloadsStyle from '../../styles/downloads'; import downloadsStyle from '../../styles/downloads';
import fileListStyle from '../../styles/fileList'; import fileListStyle from '../../styles/fileList';
@ -41,29 +42,38 @@ class DownloadsPage extends React.PureComponent {
return ( return (
<View style={downloadsStyle.container}> <View style={downloadsStyle.container}>
{!fetching && !hasDownloads && <Text style={downloadsStyle.noDownloadsText}>You have not downloaded anything from LBRY yet.</Text>} {!fetching && !hasDownloads &&
{fetching && !hasDownloads && <ActivityIndicator size="large" color={Colors.LbryGreen} style={downloadsStyle.loading} /> } <View style={downloadsStyle.busyContainer}>
<Text style={downloadsStyle.noDownloadsText}>You have not downloaded anything from LBRY yet.</Text>
</View>}
{fetching && !hasDownloads &&
<View style={downloadsStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} style={downloadsStyle.loading} />
</View>}
{hasDownloads && {hasDownloads &&
<FlatList <View style={downloadsStyle.subContainer}>
style={downloadsStyle.scrollContainer} <StorageStatsCard fileInfos={fileInfos} />
contentContainerStyle={downloadsStyle.scrollPadding} <FlatList
renderItem={ ({item}) => ( style={downloadsStyle.scrollContainer}
<FileListItem contentContainerStyle={downloadsStyle.scrollPadding}
style={fileListStyle.item} renderItem={ ({item}) => (
uri={this.uriFromFileInfo(item)} <FileListItem
navigation={navigation} style={fileListStyle.item}
onPress={() => navigateToUri(navigation, this.uriFromFileInfo(item), { autoplay: true })} /> uri={this.uriFromFileInfo(item)}
) navigation={navigation}
} onPress={() => 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; data={fileInfos.sort((a, b) => {
if (a.completed && !b.completed) return 1; // TODO: Implement sort based on user selection
if (a.metadata.title === b.metadata.title) return 0; if (!a.completed && b.completed) return -1;
return (a.metadata.title < b.metadata.title) ? -1 : 1; if (a.completed && !b.completed) return 1;
})} if (a.metadata.title === b.metadata.title) return 0;
keyExtractor={(item, index) => item.outpoint} return (a.metadata.title < b.metadata.title) ? -1 : 1;
/>} })}
keyExtractor={(item, index) => item.outpoint}
/>
</View>}
<FloatingWalletBalance navigation={navigation} /> <FloatingWalletBalance navigation={navigation} />
<UriBar navigation={navigation} /> <UriBar navigation={navigation} />
</View> </View>

View file

@ -121,7 +121,7 @@ class SplashScreen extends React.PureComponent {
const { deleteCompleteBlobs } = this.props; const { deleteCompleteBlobs } = this.props;
const startupStatus = status.startup_status; const startupStatus = status.startup_status;
// At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve // 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) { if (hasStarted) {
deleteCompleteBlobs(); deleteCompleteBlobs();

View file

@ -10,7 +10,11 @@ const Colors = {
Orange: '#ffbb00', Orange: '#ffbb00',
Red: '#ff0000', Red: '#ff0000',
VeryLightGrey: '#f1f1f1', VeryLightGrey: '#f1f1f1',
White: '#ffffff' White: '#ffffff',
StatsAudio: '#f6a637',
StatsImage: '#ff4a7d',
StatsOther: '#26bcf7'
}; };
export default Colors; export default Colors;

View file

@ -2,22 +2,29 @@ import { StyleSheet } from 'react-native';
const downloadsStyle = StyleSheet.create({ const downloadsStyle = StyleSheet.create({
container: { container: {
flex: 1
},
busyContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center',
flexDirection: 'row'
},
subContainer: {
flex: 1
}, },
itemList: { itemList: {
flex: 1, flex: 1,
}, },
scrollContainer: { scrollContainer: {
flex: 1, flex: 1,
width: '100%',
height: '100%',
paddingLeft: 16, paddingLeft: 16,
paddingRight: 16, paddingRight: 16,
marginTop: 16,
marginBottom: 60 marginBottom: 60
}, },
scrollPadding: { scrollPadding: {
marginTop: -16,
paddingBottom: 16 paddingBottom: 16
}, },
noDownloadsText: { noDownloadsText: {

View file

@ -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;

View file

@ -1,40 +1,59 @@
import { NavigationActions, StackActions } from 'react-navigation'; import { NavigationActions, StackActions } from 'react-navigation';
export function navigateToUri(navigation, uri, additionalParams) { export function dispatchNavigateToUri(dispatch, nav, uri) {
if (!navigation) { const params = { uri };
return; if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
} const mainRoute = nav.routes[0];
const discoverRoute = mainRoute.routes[0];
if (uri === navigation.state.key) { if (discoverRoute.index > 0 && 'File' === discoverRoute.routes[discoverRoute.index].routeName) {
return; 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 params = Object.assign({ uri }, additionalParams);
if ('File' === navigation.state.routeName) {
const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params }); const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params });
navigation.dispatch(stackAction); dispatch(stackAction);
return; 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) { export function formatBytes(bytes, decimalPoints = 0) {
const params = { uri }; if (!bytes) {
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) { return '0 KB';
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;
}
}
}
const navigateAction = NavigationActions.navigate({ routeName: 'File', key: uri, params }); if (bytes < 1048576) { // < 1MB
dispatch(navigateAction); 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 });
}

View file

@ -37,7 +37,7 @@ import org.renpy.android.ResourceManager;
*/ */
public class LbrynetService extends PythonService { 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"; public static final String ACTION_STOP_SERVICE = "io.lbry.browser.ACTION_STOP_SERVICE";

View file

@ -83,6 +83,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
*/ */
private boolean serviceRunning; private boolean serviceRunning;
private boolean receivedStopService;
protected String getMainComponentName() { protected String getMainComponentName() {
return "LBRYApp"; return "LBRYApp";
} }
@ -138,6 +140,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
stopServiceReceiver = new BroadcastReceiver() { stopServiceReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
MainActivity.this.receivedStopService = true;
MainActivity.this.finish(); MainActivity.this.finish();
} }
}; };
@ -383,6 +386,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
notificationManager.cancel(downloadNotificationIds.get(i)); notificationManager.cancel(downloadNotificationIds.get(i));
} }
} }
if (receivedStopService || !isServiceRunning(LbrynetService.class)) {
notificationManager.cancelAll();
}
super.onDestroy(); super.onDestroy();
if (mReactInstanceManager != null) { if (mReactInstanceManager != null) {