storage_card_widget.dart 12 KB

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