storage_card_widget.dart 12 KB

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