Merge pull request #590 from ente-io/redesign-storage-card

Redesign storage card
This commit is contained in:
Ashil 2022-10-26 09:43:22 +05:30 committed by GitHub
commit 8490d83e59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 385 additions and 192 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1285,6 +1285,19 @@ class FilesDB {
return deduplicatedFiles;
}
Future<Map<FileType, int>> fetchFilesCountbyType(int userID) async {
final db = await instance.database;
final result = await db.rawQuery(
"SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType",
);
final filesCount = <FileType, int>{};
for (var e in result) {
filesCount.addAll({getFileType(e[columnFileType]): e.values.last});
}
return filesCount;
}
Map<String, dynamic> _getRowForFile(File file) {
final row = <String, dynamic>{};
if (file.generatedID != null) {

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/subscription.dart';
class UserDetails extends Equatable {
@ -118,3 +119,19 @@ class FamilyData {
);
}
}
class FilesCount {
final Map<FileType, int> filesCount;
FilesCount(this.filesCount);
int get total =>
images + videos + livePhotos + (filesCount[getInt(FileType.other)] ?? 0);
int get photos => images + livePhotos;
int get images => filesCount[FileType.image] ?? 0;
int get videos => filesCount[FileType.video] ?? 0;
int get livePhotos => filesCount[FileType.livePhoto] ?? 0;
}

View file

@ -1,14 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class EnteLoadingWidget extends StatelessWidget {
const EnteLoadingWidget({Key? key}) : super(key: key);
final Color? color;
const EnteLoadingWidget({this.color, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox.fromSize(
size: const Size.square(30),
child: const CupertinoActivityIndicator(),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox.fromSize(
size: const Size.square(16),
child: CircularProgressIndicator(
strokeWidth: 2,
color: color ?? getEnteColorScheme(context).strokeBase,
),
),
),
);
}

View file

@ -1,10 +1,18 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/loading_widget.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/ui/settings/storage_error_widget.dart';
import 'package:photos/ui/settings/storage_progress_widget.dart';
import 'package:photos/utils/data_util.dart';
class DetailsSectionWidget extends StatefulWidget {
@ -17,6 +25,7 @@ class DetailsSectionWidget extends StatefulWidget {
class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
late Image _background;
final _logger = Logger((_DetailsSectionWidgetState).toString());
final ValueNotifier<bool> _isStorageCardPressed = ValueNotifier(false);
@override
void initState() {
@ -59,6 +68,12 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
),
);
},
onTapDown: (details) {
_isStorageCardPressed.value = true;
},
onTapUp: (details) {
_isStorageCardPressed.value = false;
},
child: containerForUserDetails(inheritedUserDetails),
);
}
@ -68,176 +83,253 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
InheritedUserDetails inheritedUserDetails,
) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 428, maxHeight: 175),
child: Stack(
children: [
Container(
width: double.infinity,
color: Colors.transparent,
child: AspectRatio(
aspectRatio: 2 / 1,
child: _background,
),
),
FutureBuilder(
future: inheritedUserDetails.userDetails,
builder: (context, snapshot) {
if (snapshot.hasData) {
return userDetails(snapshot.data as UserDetails);
}
if (snapshot.hasError) {
_logger.severe('failed to load user details', snapshot.error);
return const EnteLoadingWidget();
}
return const EnteLoadingWidget();
},
),
const Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.chevron_right,
color: Colors.white,
size: 24,
),
),
],
),
);
}
Widget userDetails(UserDetails userDetails) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 20,
left: 16,
right: 16,
),
child: Container(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
constraints: const BoxConstraints(maxWidth: 365),
child: AspectRatio(
aspectRatio: 2 / 1,
child: Stack(
children: [
_background,
FutureBuilder(
future: inheritedUserDetails.userDetails,
builder: (context, snapshot) {
if (snapshot.hasData) {
return userDetails(snapshot.data as UserDetails);
}
if (snapshot.hasError) {
_logger.severe(
'failed to load user details',
snapshot.error,
);
return const StorageErrorWidget();
}
return const EnteLoadingWidget(color: strokeBaseDark);
},
),
Align(
alignment: Alignment.topLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Storage",
style: Theme.of(context).textTheme.subtitle2!.copyWith(
color: Colors.white.withOpacity(0.7),
),
),
Text(
"${convertBytesToReadableFormat(userDetails.getFreeStorage())} of ${convertBytesToReadableFormat(userDetails.getTotalStorage())} free",
style: Theme.of(context)
.textTheme
.headline5!
.copyWith(color: Colors.white),
),
],
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 4),
child: ValueListenableBuilder<bool>(
builder: (BuildContext context, bool value, Widget? child) {
return Icon(
Icons.chevron_right_outlined,
color: value ? strokeMutedDark : strokeBaseDark,
);
},
valueListenable: _isStorageCardPressed,
),
),
),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.2),
width: MediaQuery.of(context).size.width,
height: 4,
),
Container(
color: Colors.white.withOpacity(0.75),
width: MediaQuery.of(context).size.width *
((userDetails.getFamilyOrPersonalUsage()) /
userDetails.getTotalStorage()),
height: 4,
),
Container(
color: Colors.white,
width: MediaQuery.of(context).size.width *
(userDetails.usage / userDetails.getTotalStorage()),
height: 4,
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
userDetails.isPartOfFamily()
? Row(
children: [
Container(
width: 8.71,
height: 8.99,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"You",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
const Padding(
padding: EdgeInsets.only(right: 12),
),
Container(
width: 8.71,
height: 8.99,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.75),
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"Family",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
],
)
: Text(
"${convertBytesToReadableFormat(userDetails.getFamilyOrPersonalUsage())} used",
style:
Theme.of(context).textTheme.bodyText1!.copyWith(
color: Colors.white,
fontSize: 12,
),
),
],
),
],
)
],
),
),
);
}
Widget userDetails(UserDetails userDetails) {
const hundredMBinBytes = 107374182;
final isMobileScreenSmall = MediaQuery.of(context).size.width <= 365;
final freeSpaceInBytes = userDetails.getFreeStorage();
final shouldShowFreeSpaceInMBs = freeSpaceInBytes < hundredMBinBytes;
final usedSpaceInGB =
convertBytesToGBs(userDetails.getFamilyOrPersonalUsage());
final totalStorageInGB = convertBytesToGBs(userDetails.getTotalStorage());
return Padding(
padding: EdgeInsets.fromLTRB(
16,
20,
16,
isMobileScreenSmall
? userDetails.isPartOfFamily()
? 12
: 8
: userDetails.isPartOfFamily()
? 20
: 12,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.topLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isMobileScreenSmall ? "Used space" : "Storage",
style: getEnteTextTheme(context)
.small
.copyWith(color: textMutedDark),
),
const SizedBox(height: 2),
RichText(
overflow: TextOverflow.ellipsis,
maxLines: 1,
text: TextSpan(
style: getEnteTextTheme(context)
.h3Bold
.copyWith(color: textBaseDark),
children: [
TextSpan(text: usedSpaceInGB.toString()),
TextSpan(text: isMobileScreenSmall ? "/" : " GB of "),
TextSpan(text: totalStorageInGB.toString() + " GB"),
TextSpan(text: isMobileScreenSmall ? "" : " used"),
],
),
),
],
),
),
Column(
children: [
Stack(
children: <Widget>[
const StorageProgressWidget(
color:
Color.fromRGBO(255, 255, 255, 0.2), //hardcoded in figma
fractionOfStorage: 1,
),
userDetails.isPartOfFamily()
? StorageProgressWidget(
color: strokeBaseDark,
fractionOfStorage:
((userDetails.getFamilyOrPersonalUsage()) /
userDetails.getTotalStorage()),
)
: const SizedBox.shrink(),
StorageProgressWidget(
color: userDetails.isPartOfFamily()
? getEnteColorScheme(context).primary300
: strokeBaseDark,
fractionOfStorage:
(userDetails.usage / userDetails.getTotalStorage()),
)
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
userDetails.isPartOfFamily()
? Row(
children: [
Container(
width: 8.71,
height: 8.99,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: getEnteColorScheme(context).primary300,
),
),
const SizedBox(width: 4),
Text(
"You",
style: getEnteTextTheme(context)
.miniBold
.copyWith(color: textBaseDark),
),
const SizedBox(width: 12),
Container(
width: 8.71,
height: 8.99,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: textBaseDark,
),
),
const SizedBox(width: 4),
Text(
"Family",
style: getEnteTextTheme(context)
.miniBold
.copyWith(color: textBaseDark),
),
],
)
: FutureBuilder(
future: FilesDB.instance.fetchFilesCountbyType(
Configuration.instance.getUserID(),
),
builder: (context, snapshot) {
if (snapshot.hasData) {
final filesCount = FilesCount(
snapshot.data as Map<FileType, int>,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${NumberFormat().format(filesCount.photos)} photos",
style: getEnteTextTheme(context)
.mini
.copyWith(color: textBaseDark),
),
Text(
"${NumberFormat().format(filesCount.videos)} videos",
style: getEnteTextTheme(context)
.mini
.copyWith(color: textBaseDark),
),
],
);
} else if (snapshot.hasError) {
_logger.severe(
'Error fetching photo and video count',
snapshot.error,
);
return const SizedBox.shrink();
} else {
return const EnteLoadingWidget(
color: strokeBaseDark,
);
}
},
),
RichText(
text: TextSpan(
style: getEnteTextTheme(context)
.mini
.copyWith(color: textFaintDark),
children: [
TextSpan(
text:
"${shouldShowFreeSpaceInMBs ? convertBytesToMBs(freeSpaceInBytes) : _roundedFreeSpace(totalStorageInGB, usedSpaceInGB)}",
),
TextSpan(
text: shouldShowFreeSpaceInMBs
? " MB free"
: " GB free",
)
],
),
),
],
),
],
)
],
),
);
}
num _roundedFreeSpace(num totalStorageInGB, num usedSpaceInGB) {
int fractionDigits;
//subtracting usedSpace from totalStorage in GB instead of converting from bytes so that free space and used space adds up in the UI
final freeSpace = totalStorageInGB - usedSpaceInGB;
//show one decimal place if free space is less than 10GB
if (freeSpace < 10) {
fractionDigits = 1;
} else {
fractionDigits = 0;
}
//omit decimal if decimal is 0
if (fractionDigits == 1 && freeSpace.remainder(1) == 0) {
fractionDigits = 0;
}
return num.parse(freeSpace.toStringAsFixed(fractionDigits));
}
}

View file

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/loading_widget.dart';
@ -27,30 +29,22 @@ class SettingsTitleBarWidget extends StatelessWidget {
icon: const Icon(Icons.keyboard_double_arrow_left_outlined),
),
FutureBuilder(
future: InheritedUserDetails.of(context)?.userDetails,
future: FilesDB.instance
.fetchFilesCountbyType(Configuration.instance.getUserID()),
builder: (context, snapshot) {
if (InheritedUserDetails.of(context) == null) {
logger.severe(
(InheritedUserDetails).toString() +
' not found before ' +
(SettingsTitleBarWidget).toString() +
' on tree',
);
throw Error();
}
if (snapshot.hasData) {
final userDetails = snapshot.data as UserDetails;
final totalFiles =
FilesCount(snapshot.data as Map<FileType, int>).total;
return Text(
"${NumberFormat().format(userDetails.fileCount)} memories",
totalFiles == 0
? "No memories yet"
: "${NumberFormat().format(totalFiles)} memories",
style: getEnteTextTheme(context).largeBold,
);
} else if (snapshot.hasError) {
logger.severe('failed to fetch filesCount');
}
if (snapshot.hasError) {
logger.severe('failed to load user details');
return const EnteLoadingWidget();
} else {
return const EnteLoadingWidget();
}
return const EnteLoadingWidget();
},
)
],

View file

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
class StorageErrorWidget extends StatelessWidget {
const StorageErrorWidget({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(
Icons.error_outline_outlined,
color: strokeBaseDark,
),
const SizedBox(height: 8),
Text(
"Your storage details could not be fetched",
style: getEnteTextTheme(context).small.copyWith(
color: textMutedDark,
),
),
],
),
);
}
}

View file

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class StorageProgressWidget extends StatelessWidget {
final Color color;
final double fractionOfStorage;
const StorageProgressWidget({
required this.color,
required this.fractionOfStorage,
super.key,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constrains) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: color,
),
width: constrains.maxWidth * fractionOfStorage,
height: 4,
);
},
);
}
}

View file

@ -1,11 +1,5 @@
import 'dart:math';
double convertBytesToGBs(final int bytes, {int precision = 2}) {
return double.parse(
(bytes / (1024 * 1024 * 1024)).toStringAsFixed(precision),
);
}
final storageUnits = ["bytes", "KB", "MB", "GB"];
String convertBytesToReadableFormat(int bytes) {
@ -24,3 +18,19 @@ String formatBytes(int bytes, [int decimals = 2]) {
final int i = (log(bytes) / log(k)).floor();
return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + storageUnits[i];
}
//shows decimals only if less than 10GB & omits decimal if decimal is 0
num convertBytesToGBs(int bytes) {
const tenGBinBytes = 10737418240;
int precision = 0;
if (bytes < tenGBinBytes) {
precision = 1;
}
final bytesInGB =
num.parse((bytes / (pow(1024, 3))).toStringAsPrecision(precision));
return bytesInGB;
}
int convertBytesToMBs(int bytes) {
return (bytes / pow(1024, 2)).round();
}