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 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>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
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,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 });
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue