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:
parent
c0b464ae36
commit
404647c4cb
10 changed files with 321 additions and 59 deletions
4
app/src/component/storageStatsCard/index.js
Normal file
4
app/src/component/storageStatsCard/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import StorageStatsCard from './view';
|
||||||
|
|
||||||
|
export default connect()(StorageStatsCard);
|
128
app/src/component/storageStatsCard/view.js
Normal file
128
app/src/component/storageStatsCard/view.js
Normal 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;
|
|
@ -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,9 +42,17 @@ 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 &&
|
||||||
|
<View style={downloadsStyle.subContainer}>
|
||||||
|
<StorageStatsCard fileInfos={fileInfos} />
|
||||||
<FlatList
|
<FlatList
|
||||||
style={downloadsStyle.scrollContainer}
|
style={downloadsStyle.scrollContainer}
|
||||||
contentContainerStyle={downloadsStyle.scrollPadding}
|
contentContainerStyle={downloadsStyle.scrollPadding}
|
||||||
|
@ -63,7 +72,8 @@ class DownloadsPage extends React.PureComponent {
|
||||||
return (a.metadata.title < b.metadata.title) ? -1 : 1;
|
return (a.metadata.title < b.metadata.title) ? -1 : 1;
|
||||||
})}
|
})}
|
||||||
keyExtractor={(item, index) => item.outpoint}
|
keyExtractor={(item, index) => item.outpoint}
|
||||||
/>}
|
/>
|
||||||
|
</View>}
|
||||||
<FloatingWalletBalance navigation={navigation} />
|
<FloatingWalletBalance navigation={navigation} />
|
||||||
<UriBar navigation={navigation} />
|
<UriBar navigation={navigation} />
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
84
app/src/styles/storageStats.js
Normal file
84
app/src/styles/storageStats.js
Normal 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;
|
|
@ -1,24 +1,5 @@
|
||||||
import { NavigationActions, StackActions } from 'react-navigation';
|
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) {
|
|
||||||
const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params });
|
|
||||||
navigation.dispatch(stackAction);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigation.navigate({ routeName: 'File', key: uri, params });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dispatchNavigateToUri(dispatch, nav, uri) {
|
export function dispatchNavigateToUri(dispatch, nav, uri) {
|
||||||
const params = { uri };
|
const params = { uri };
|
||||||
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
|
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
|
||||||
|
@ -38,3 +19,41 @@ export function dispatchNavigateToUri(dispatch, nav, uri) {
|
||||||
const navigateAction = NavigationActions.navigate({ routeName: 'File', key: uri, params });
|
const navigateAction = NavigationActions.navigate({ routeName: 'File', key: uri, params });
|
||||||
dispatch(navigateAction);
|
dispatch(navigateAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes, decimalPoints = 0) {
|
||||||
|
if (!bytes) {
|
||||||
|
return '0 KB';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue