details_section_widget.dart 10 KB

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