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 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 (
<View style={downloadsStyle.container}>
{!fetching && !hasDownloads && <Text style={downloadsStyle.noDownloadsText}>You have not downloaded anything from LBRY yet.</Text>}
{fetching && !hasDownloads && <ActivityIndicator size="large" color={Colors.LbryGreen} style={downloadsStyle.loading} /> }
{!fetching && !hasDownloads &&
<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 &&
<FlatList
style={downloadsStyle.scrollContainer}
contentContainerStyle={downloadsStyle.scrollPadding}
renderItem={ ({item}) => (
<FileListItem
style={fileListStyle.item}
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;
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}
/>}
<View style={downloadsStyle.subContainer}>
<StorageStatsCard fileInfos={fileInfos} />
<FlatList
style={downloadsStyle.scrollContainer}
contentContainerStyle={downloadsStyle.scrollPadding}
renderItem={ ({item}) => (
<FileListItem
style={fileListStyle.item}
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;
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}
/>
</View>}
<FloatingWalletBalance navigation={navigation} />
<UriBar navigation={navigation} />
</View>

View file

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

View file

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

View file

@ -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: {

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';
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);
}
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 });
}

View file

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

View file

@ -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) {