storage_card_widget.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. import 'package:logging/logging.dart';
  4. import 'package:photos/core/constants.dart';
  5. import 'package:photos/models/user_details.dart';
  6. import 'package:photos/states/user_details_state.dart';
  7. import 'package:photos/theme/colors.dart';
  8. import 'package:photos/theme/ente_theme.dart';
  9. import 'package:photos/ui/common/loading_widget.dart';
  10. import 'package:photos/ui/payment/subscription.dart';
  11. import 'package:photos/ui/settings/storage_progress_widget.dart';
  12. import 'package:photos/utils/data_util.dart';
  13. class StorageCardWidget extends StatefulWidget {
  14. const StorageCardWidget({Key? key}) : super(key: key);
  15. @override
  16. State<StorageCardWidget> createState() => _StorageCardWidgetState();
  17. }
  18. class _StorageCardWidgetState extends State<StorageCardWidget> {
  19. late Image _background;
  20. final _logger = Logger((_StorageCardWidgetState).toString());
  21. final ValueNotifier<bool> _isStorageCardPressed = ValueNotifier(false);
  22. @override
  23. void initState() {
  24. super.initState();
  25. _background = const Image(
  26. image: AssetImage("assets/storage_card_background.png"),
  27. fit: BoxFit.fill,
  28. );
  29. }
  30. @override
  31. void didChangeDependencies() {
  32. super.didChangeDependencies();
  33. // precache background image to avoid flicker
  34. // https://stackoverflow.com/questions/51343735/flutter-image-preload
  35. precacheImage(_background.image, context);
  36. }
  37. @override
  38. void dispose() {
  39. _isStorageCardPressed.dispose();
  40. super.dispose();
  41. }
  42. @override
  43. Widget build(BuildContext context) {
  44. final inheritedUserDetails = InheritedUserDetails.of(context);
  45. final userDetails = inheritedUserDetails?.userDetails;
  46. if (inheritedUserDetails == null) {
  47. _logger.severe(
  48. (InheritedUserDetails).toString() + 'is null',
  49. );
  50. throw Error();
  51. } else {
  52. return GestureDetector(
  53. behavior: HitTestBehavior.translucent,
  54. onTap: () async {
  55. Navigator.of(context).push(
  56. MaterialPageRoute(
  57. builder: (BuildContext context) {
  58. return getSubscriptionPage();
  59. },
  60. ),
  61. );
  62. },
  63. onTapDown: (details) => _isStorageCardPressed.value = true,
  64. onTapCancel: () => _isStorageCardPressed.value = false,
  65. onTapUp: (details) => _isStorageCardPressed.value = false,
  66. child: containerForUserDetails(userDetails),
  67. );
  68. }
  69. }
  70. Widget containerForUserDetails(
  71. UserDetails? userDetails,
  72. ) {
  73. return ConstrainedBox(
  74. constraints: const BoxConstraints(maxWidth: 350),
  75. child: AspectRatio(
  76. aspectRatio: 2 / 1,
  77. child: Stack(
  78. children: [
  79. _background,
  80. userDetails is UserDetails
  81. ? _userDetails(userDetails)
  82. : const EnteLoadingWidget(
  83. color: strokeBaseDark,
  84. ),
  85. Align(
  86. alignment: Alignment.centerRight,
  87. child: Padding(
  88. padding: const EdgeInsets.only(right: 4),
  89. child: ValueListenableBuilder<bool>(
  90. builder: (BuildContext context, bool value, Widget? child) {
  91. return Icon(
  92. Icons.chevron_right_outlined,
  93. color: value ? strokeMutedDark : strokeBaseDark,
  94. );
  95. },
  96. valueListenable: _isStorageCardPressed,
  97. ),
  98. ),
  99. ),
  100. ],
  101. ),
  102. ),
  103. );
  104. }
  105. Widget _userDetails(UserDetails userDetails) {
  106. const hundredMBinBytes = 107374182;
  107. const oneTBinBytes = 1073741824000;
  108. final usedStorageInBytes = userDetails.getFamilyOrPersonalUsage();
  109. final totalStorageInBytes = userDetails.getTotalStorage();
  110. final freeStorageInBytes = totalStorageInBytes - usedStorageInBytes;
  111. final isMobileScreenSmall =
  112. MediaQuery.of(context).size.width <= mobileSmallThreshold;
  113. final shouldShowFreeSpaceInMBs = freeStorageInBytes < hundredMBinBytes;
  114. final shouldShowFreeSpaceInTBs = freeStorageInBytes >= oneTBinBytes;
  115. final shouldShowUsedStorageInTBs = usedStorageInBytes >= oneTBinBytes;
  116. final shouldShowTotalStorageInTBs = totalStorageInBytes >= oneTBinBytes;
  117. final shouldShowUsedStorageInMBs = usedStorageInBytes < hundredMBinBytes;
  118. final usedStorageInGB = roundBytesUsedToGBs(
  119. usedStorageInBytes,
  120. freeStorageInBytes,
  121. );
  122. final totalStorageInGB = convertBytesToGBs(totalStorageInBytes).truncate();
  123. final usedStorageInTB = roundGBsToTBs(usedStorageInGB);
  124. final totalStorageInTB = roundGBsToTBs(totalStorageInGB);
  125. return Padding(
  126. padding: EdgeInsets.fromLTRB(
  127. 16,
  128. 20,
  129. 16,
  130. isMobileScreenSmall ? 12 : 20,
  131. ),
  132. child: Column(
  133. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  134. children: [
  135. Align(
  136. alignment: Alignment.topLeft,
  137. child: Column(
  138. crossAxisAlignment: CrossAxisAlignment.start,
  139. children: [
  140. Text(
  141. isMobileScreenSmall ? "Used space" : "Storage",
  142. style: getEnteTextTheme(context)
  143. .small
  144. .copyWith(color: textMutedDark),
  145. ),
  146. const SizedBox(height: 2),
  147. RichText(
  148. overflow: TextOverflow.ellipsis,
  149. maxLines: 1,
  150. text: TextSpan(
  151. style: getEnteTextTheme(context)
  152. .h3Bold
  153. .copyWith(color: textBaseDark),
  154. children: _usedStorageDetails(
  155. isMobileScreenSmall: isMobileScreenSmall,
  156. shouldShowTotalStorageInTBs: shouldShowTotalStorageInTBs,
  157. shouldShowUsedStorageInTBs: shouldShowUsedStorageInTBs,
  158. shouldShowUsedStorageInMBs: shouldShowUsedStorageInMBs,
  159. usedStorageInBytes: usedStorageInBytes,
  160. usedStorageInGB: usedStorageInGB,
  161. totalStorageInTB: totalStorageInTB,
  162. usedStorageInTB: usedStorageInTB,
  163. totalStorageInGB: totalStorageInGB,
  164. ),
  165. ),
  166. ),
  167. ],
  168. ),
  169. ),
  170. Column(
  171. children: [
  172. Stack(
  173. children: <Widget>[
  174. const StorageProgressWidget(
  175. color:
  176. Color.fromRGBO(255, 255, 255, 0.2), //hardcoded in figma
  177. fractionOfStorage: 1,
  178. ),
  179. userDetails.isPartOfFamily()
  180. ? StorageProgressWidget(
  181. color: strokeBaseDark,
  182. fractionOfStorage:
  183. ((userDetails.getFamilyOrPersonalUsage()) /
  184. userDetails.getTotalStorage()),
  185. )
  186. : const SizedBox.shrink(),
  187. StorageProgressWidget(
  188. color: userDetails.isPartOfFamily()
  189. ? getEnteColorScheme(context).primary300
  190. : strokeBaseDark,
  191. fractionOfStorage:
  192. (userDetails.usage / userDetails.getTotalStorage()),
  193. )
  194. ],
  195. ),
  196. const SizedBox(height: 12),
  197. Row(
  198. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  199. crossAxisAlignment: CrossAxisAlignment.start,
  200. children: [
  201. userDetails.isPartOfFamily()
  202. ? Row(
  203. children: [
  204. Container(
  205. width: 8.71,
  206. height: 8.99,
  207. decoration: BoxDecoration(
  208. shape: BoxShape.circle,
  209. color: getEnteColorScheme(context).primary300,
  210. ),
  211. ),
  212. const SizedBox(width: 4),
  213. Text(
  214. "You",
  215. style: getEnteTextTheme(context)
  216. .miniBold
  217. .copyWith(color: textBaseDark),
  218. ),
  219. const SizedBox(width: 12),
  220. Container(
  221. width: 8.71,
  222. height: 8.99,
  223. decoration: const BoxDecoration(
  224. shape: BoxShape.circle,
  225. color: textBaseDark,
  226. ),
  227. ),
  228. const SizedBox(width: 4),
  229. Text(
  230. "Family",
  231. style: getEnteTextTheme(context)
  232. .miniBold
  233. .copyWith(color: textBaseDark),
  234. ),
  235. ],
  236. )
  237. : const SizedBox.shrink(),
  238. RichText(
  239. text: TextSpan(
  240. style: getEnteTextTheme(context)
  241. .mini
  242. .copyWith(color: textFaintDark),
  243. children: [
  244. TextSpan(
  245. text:
  246. "${shouldShowFreeSpaceInMBs ? max(0, convertBytesToMBs(freeStorageInBytes)) : _roundedFreeSpace(totalStorageInGB, usedStorageInGB)}",
  247. ),
  248. TextSpan(
  249. text: shouldShowFreeSpaceInTBs
  250. ? " TB free"
  251. : shouldShowFreeSpaceInMBs
  252. ? " MB free"
  253. : " GB free",
  254. )
  255. ],
  256. ),
  257. ),
  258. ],
  259. ),
  260. ],
  261. )
  262. ],
  263. ),
  264. );
  265. }
  266. num _roundedFreeSpace(num totalStorageInGB, num usedStorageInGB) {
  267. int fractionDigits;
  268. //subtracting usedSpace from totalStorage in GB instead of converting from bytes so that free space and used space adds up in the UI
  269. final freeStorage = totalStorageInGB - usedStorageInGB;
  270. if (freeStorage >= 1000) {
  271. return roundGBsToTBs(freeStorage);
  272. }
  273. //show one decimal place if free space is less than 10GB
  274. if (freeStorage < 10) {
  275. fractionDigits = 1;
  276. } else {
  277. fractionDigits = 0;
  278. }
  279. //omit decimal if decimal is 0
  280. if (fractionDigits == 1 && freeStorage.remainder(1) == 0) {
  281. fractionDigits = 0;
  282. }
  283. return num.parse(freeStorage.toStringAsFixed(fractionDigits));
  284. }
  285. List<TextSpan> _usedStorageDetails({
  286. @required isMobileScreenSmall,
  287. @required shouldShowUsedStorageInTBs,
  288. @required shouldShowTotalStorageInTBs,
  289. @required shouldShowUsedStorageInMBs,
  290. @required usedStorageInBytes,
  291. @required usedStorageInGB,
  292. @required totalStorageInGB,
  293. @required usedStorageInTB,
  294. @required totalStorageInTB,
  295. }) {
  296. if (isMobileScreenSmall) {
  297. return [
  298. TextSpan(text: usedStorageInGB.toString() + "/"),
  299. TextSpan(text: totalStorageInGB.toString() + " GB"),
  300. ];
  301. }
  302. return [
  303. TextSpan(
  304. text: shouldShowUsedStorageInTBs
  305. ? usedStorageInTB.toString() + " TB of "
  306. : shouldShowUsedStorageInMBs
  307. ? convertBytesToMBs(usedStorageInBytes).toString() + " MB of "
  308. : usedStorageInGB.toString() + " GB of ",
  309. ),
  310. TextSpan(
  311. text: shouldShowTotalStorageInTBs
  312. ? totalStorageInTB.toString() + " TB used"
  313. : totalStorageInGB.toString() + " GB used",
  314. ),
  315. ];
  316. }
  317. }