storage_card_widget.dart 12 KB

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