Przeglądaj źródła

Merge branch 'master' into title-bar-component

ashilkn 2 lat temu
rodzic
commit
1c83a2095b

+ 2 - 2
android/app/build.gradle

@@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 32
+    compileSdkVersion 33
 
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
@@ -47,7 +47,7 @@ android {
     defaultConfig {
         applicationId "io.ente.photos"
         minSdkVersion 19
-        targetSdkVersion 30
+        targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

+ 55 - 29
android/app/src/main/AndroidManifest.xml

@@ -1,65 +1,91 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.ente.photos">
-    <application android:name="${applicationName}" android:label="@string/app_name" android:icon="@mipmap/launcher_icon" android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" android:largeHeap="true">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="io.ente.photos">
+    <application android:name="${applicationName}"
+                 android:label="@string/app_name"
+                 android:icon="@mipmap/launcher_icon"
+                 android:usesCleartextTraffic="true"
+                 android:requestLegacyExternalStorage="true"
+                 android:allowBackup="false"
+                 android:fullBackupContent="false"
+                 android:largeHeap="true">
 
-        <activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
+        <activity android:name=".MainActivity" android:launchMode="singleTop"
+                  android:theme="@style/LaunchTheme"
+                  android:exported="true"
+                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+                  android:hardwareAccelerated="true"
+                  android:windowSoftInputMode="adjustResize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
 
             <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
                 <data android:scheme="ente"/>
             </intent-filter>
 
             <!--Filter to support sharing images into our app-->
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="image/*" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="image/*"/>
             </intent-filter>
 
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND_MULTIPLE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="image/*" />
+                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="image/*"/>
             </intent-filter>
 
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="video/*" />
+                <action android:name="android.intent.action.SEND"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="video/*"/>
             </intent-filter>
 
             <intent-filter android:label="@string/backup">
-                <action android:name="android.intent.action.SEND_MULTIPLE" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="video/*" />
+                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="video/*"/>
             </intent-filter>
 
         </activity>
         <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
-        <meta-data android:name="flutterEmbedding" android:value="2" />
-        <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
-        <meta-data android:name="io.sentry.dsn" android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4" />
-        <meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
+        <meta-data android:name="flutterEmbedding" android:value="2"/>
+        <meta-data android:name="asset_statements"
+                   android:resource="@string/asset_statements"/>
+        <meta-data android:name="io.sentry.dsn"
+                   android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4"/>
+        <meta-data android:name="firebase_analytics_collection_deactivated"
+                   android:value="true"/>
     </application>
 
     <!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
     <!-- https://developer.android.com/training/package-visibility/use-cases -->
     <queries>
         <intent>
-            <action android:name="android.intent.action.SENDTO" />
-            <data android:scheme="mailto" />
+            <action android:name="android.intent.action.SENDTO"/>
+            <data android:scheme="mailto"/>
         </intent>
     </queries>
     <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="com.android.vending.BILLING" />
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+    <uses-permission
+            android:name="android.permission.READ_MEDIA_IMAGES"/> <!-- If you want to read images-->
+    <uses-permission
+            android:name="android.permission.READ_MEDIA_VIDEO"/> <!-- If you want to read videos-->
+    <uses-permission
+            android:name="android.permission.READ_EXTERNAL_STORAGE"
+            android:maxSdkVersion="32"/>
+    <uses-permission
+            android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+            android:maxSdkVersion="29"
+            tools:ignore="ScopedStorage"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="com.android.vending.BILLING"/>
 </manifest>

BIN
assets/2.0x/storage_card_background.png


BIN
assets/3.0x/storage_card_background.png


BIN
assets/storage_card_background.png


+ 20 - 34
lib/db/files_db.dart

@@ -12,7 +12,6 @@ import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.dart';
-import 'package:photos/services/feature_flag_service.dart';
 import 'package:photos/utils/file_uploader_util.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:sqflite_migration/sqflite_migration.dart';
@@ -611,17 +610,9 @@ class FilesDB {
   }) async {
     final db = await instance.database;
     final order = (asc ?? false ? 'ASC' : 'DESC');
-    String whereClause;
-    List<Object> whereArgs;
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
-      whereClause =
-          '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnMMdVisibility = ?';
-      whereArgs = [collectionID, startTime, endTime, visibility];
-    } else {
-      whereClause =
-          '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
-      whereArgs = [collectionID, startTime, endTime];
-    }
+    const String whereClause =
+        '$columnCollectionID = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ?';
+    final List<Object> whereArgs = [collectionID, startTime, endTime];
 
     final results = await db.query(
       filesTable,
@@ -1080,7 +1071,9 @@ class FilesDB {
     final db = await instance.database;
     final count = Sqflite.firstIntValue(
       await db.rawQuery(
-        'SELECT COUNT(*) FROM $filesTable where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID',
+        'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where '
+        '$columnMMdVisibility'
+        ' = $visibility AND $columnOwnerID = $ownerID',
       ),
     );
     return count;
@@ -1143,25 +1136,7 @@ class FilesDB {
 
   Future<List<File>> getLatestCollectionFiles() async {
     debugPrint("Fetching latestCollectionFiles from db");
-    String query;
-    if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
-      query = '''
-      SELECT $filesTable.*
-      FROM $filesTable
-      INNER JOIN
-        (
-          SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time
-          FROM $filesTable
-          WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS 
-          NOT -1 AND $columnMMdVisibility = $visibilityVisible AND 
-          $columnUploadedFileID IS NOT -1)
-          GROUP BY $columnCollectionID
-        ) latest_files
-        ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
-        AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
-    ''';
-    } else {
-      query = '''
+    const String query = '''
       SELECT $filesTable.*
       FROM $filesTable
       INNER JOIN
@@ -1173,9 +1148,7 @@ class FilesDB {
         ) latest_files
         ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID
         AND $filesTable.$columnCreationTime = latest_files.max_creation_time;
-
   ''';
-    }
     final db = await instance.database;
     final rows = await db.rawQuery(
       query,
@@ -1285,6 +1258,19 @@ class FilesDB {
     return deduplicatedFiles;
   }
 
+  Future<Map<FileType, int>> fetchFilesCountbyType(int userID) async {
+    final db = await instance.database;
+    final result = await db.rawQuery(
+      "SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType",
+    );
+
+    final filesCount = <FileType, int>{};
+    for (var e in result) {
+      filesCount.addAll({getFileType(e[columnFileType]): e.values.last});
+    }
+    return filesCount;
+  }
+
   Map<String, dynamic> _getRowForFile(File file) {
     final row = <String, dynamic>{};
     if (file.generatedID != null) {

+ 6 - 0
lib/events/tab_changed_event.dart

@@ -16,3 +16,9 @@ enum TabChangedEventSource {
   collectionsPage,
   backButton,
 }
+
+class TabDoubleTapEvent extends Event {
+  final int selectedIndex;
+
+  TabDoubleTapEvent(this.selectedIndex);
+}

+ 17 - 0
lib/models/user_details.dart

@@ -2,6 +2,7 @@ import 'dart:math';
 
 import 'package:collection/collection.dart';
 import 'package:equatable/equatable.dart';
+import 'package:photos/models/file_type.dart';
 import 'package:photos/models/subscription.dart';
 
 class UserDetails extends Equatable {
@@ -118,3 +119,19 @@ class FamilyData {
     );
   }
 }
+
+class FilesCount {
+  final Map<FileType, int> filesCount;
+  FilesCount(this.filesCount);
+
+  int get total =>
+      images + videos + livePhotos + (filesCount[getInt(FileType.other)] ?? 0);
+
+  int get photos => images + livePhotos;
+
+  int get images => filesCount[FileType.image] ?? 0;
+
+  int get videos => filesCount[FileType.video] ?? 0;
+
+  int get livePhotos => filesCount[FileType.livePhoto] ?? 0;
+}

+ 7 - 5
lib/theme/text_style.dart

@@ -16,11 +16,7 @@ const TextStyle brandStyleMedium = TextStyle(
   fontFamily: 'Montserrat',
   fontSize: 24,
 );
-const TextStyle brandStyleLarge = TextStyle(
-  fontWeight: FontWeight.bold,
-  fontFamily: 'Montserrat',
-  fontSize: 28,
-);
+
 const TextStyle h1 = TextStyle(
   fontSize: 48,
   height: 48 / 28,
@@ -87,6 +83,8 @@ class EnteTextTheme {
   final TextStyle miniBold;
   final TextStyle tiny;
   final TextStyle tinyBold;
+  final TextStyle brandSmall;
+  final TextStyle brandMedium;
 
   const EnteTextTheme({
     required this.h1,
@@ -105,6 +103,8 @@ class EnteTextTheme {
     required this.miniBold,
     required this.tiny,
     required this.tinyBold,
+    required this.brandSmall,
+    required this.brandMedium,
   });
 }
 
@@ -129,5 +129,7 @@ EnteTextTheme _buildEnteTextStyle(Color color) {
     miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
     tiny: tiny.copyWith(color: color),
     tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
+    brandSmall: brandStyleSmall.copyWith(color: color),
+    brandMedium: brandStyleMedium.copyWith(color: color),
   );
 }

+ 18 - 14
lib/ui/collections/section_title.dart

@@ -40,18 +40,22 @@ class SectionTitle extends StatelessWidget {
   }
 }
 
-RichText onEnteSection = RichText(
-  text: const TextSpan(
-    children: [
-      TextSpan(
-        text: "On ",
-        style: TextStyle(
-          fontWeight: FontWeight.w600,
-          fontFamily: 'Inter',
-          fontSize: 21,
+RichText getOnEnteSection(BuildContext context) {
+  final EnteTextTheme textTheme = getEnteTextTheme(context);
+  return RichText(
+    text: TextSpan(
+      children: [
+        TextSpan(
+          text: "On ",
+          style: TextStyle(
+            fontWeight: FontWeight.w600,
+            fontFamily: 'Inter',
+            fontSize: 21,
+            color: textTheme.brandSmall.color,
+          ),
         ),
-      ),
-      TextSpan(text: "ente", style: brandStyleSmall),
-    ],
-  ),
-);
+        TextSpan(text: "ente", style: textTheme.brandSmall),
+      ],
+    ),
+  );
+}

+ 1 - 1
lib/ui/collections_gallery_widget.dart

@@ -140,7 +140,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               crossAxisAlignment: CrossAxisAlignment.end,
               children: [
-                SectionTitle(titleWithBrand: onEnteSection),
+                SectionTitle(titleWithBrand: getOnEnteSection(context)),
                 _sortMenu(),
               ],
             ),

+ 13 - 4
lib/ui/common/loading_widget.dart

@@ -1,14 +1,23 @@
 import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/theme/ente_theme.dart';
 
 class EnteLoadingWidget extends StatelessWidget {
-  const EnteLoadingWidget({Key? key}) : super(key: key);
+  final Color? color;
+  const EnteLoadingWidget({this.color, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return Center(
-      child: SizedBox.fromSize(
-        size: const Size.square(30),
-        child: const CupertinoActivityIndicator(),
+      child: Padding(
+        padding: const EdgeInsets.all(8.0),
+        child: SizedBox.fromSize(
+          size: const Size.square(16),
+          child: CircularProgressIndicator(
+            strokeWidth: 2,
+            color: color ?? getEnteColorScheme(context).strokeBase,
+          ),
+        ),
       ),
     );
   }

+ 15 - 4
lib/ui/home/home_bottom_nav_bar.dart

@@ -41,12 +41,17 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
     _tabChangedEventSubscription =
         Bus.instance.on<TabChangedEvent>().listen((event) {
       if (event.source != TabChangedEventSource.tabBar) {
-        debugPrint('index changed to ${event.selectedIndex}');
+        debugPrint('${(TabChangedEvent).toString()} index changed  from '
+            '$currentTabIndex to ${event.selectedIndex} via ${event.source}');
         if (mounted) {
           setState(() {
             currentTabIndex = event.selectedIndex;
           });
         }
+      } else if (event.source == TabChangedEventSource.tabBar &&
+          currentTabIndex == event.selectedIndex) {
+        // user tapped on the currently selected index on the tapBar
+        Bus.instance.fire(TabDoubleTapEvent(currentTabIndex));
       }
     });
   }
@@ -57,7 +62,8 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
     super.dispose();
   }
 
-  void _onTabChange(int index) {
+  void _onTabChange(int index, {String mode = 'tabChanged'}) {
+    debugPrint("_TabChanged called via method $mode");
     Bus.instance.fire(
       TabChangedEvent(
         index,
@@ -130,6 +136,7 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
                                 onPressed: () {
                                   _onTabChange(
                                     0,
+                                    mode: "OnPressed",
                                   ); // To take care of occasional missing events
                                 },
                               ),
@@ -142,7 +149,9 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
                                 onPressed: () {
                                   _onTabChange(
                                     1,
-                                  ); // To take care of occasional missing events
+                                    mode: "OnPressed",
+                                  ); // To take care of occasional missing
+                                  // events
                                 },
                               ),
                               GButton(
@@ -154,7 +163,9 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
                                 onPressed: () {
                                   _onTabChange(
                                     2,
-                                  ); // To take care of occasional missing events
+                                    mode: "OnPressed",
+                                  ); // To take care
+                                  // of occasional missing events
                                 },
                               ),
                             ],

+ 3 - 0
lib/ui/home/home_gallery_widget.dart

@@ -27,6 +27,7 @@ class HomeGalleryWidget extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    double bottomSafeArea = MediaQuery.of(context).padding.bottom;
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
         final ownerID = Configuration.instance.getUserID();
@@ -78,6 +79,8 @@ class HomeGalleryWidget extends StatelessWidget {
       selectedFiles: selectedFiles,
       header: header,
       footer: footer,
+      // scrollSafe area -> SafeArea + Preserver more + Nav Bar buttons
+      scrollBottomSafeArea: bottomSafeArea + 180,
     );
     return gallery;
   }

+ 3 - 0
lib/ui/home_widget.dart

@@ -88,7 +88,10 @@ class _HomeWidgetState extends State<HomeWidget> {
     _tabChangedEventSubscription =
         Bus.instance.on<TabChangedEvent>().listen((event) {
       if (event.source != TabChangedEventSource.pageView) {
+        debugPrint(
+            "TabChange going from $_selectedTabIndex to ${event.selectedIndex} souce: ${event.source}");
         _selectedTabIndex = event.selectedIndex;
+        // _pageController.jumpToPage(_selectedTabIndex);
         _pageController.animateToPage(
           event.selectedIndex,
           duration: const Duration(milliseconds: 100),

+ 4 - 1
lib/ui/huge_listview/draggable_scrollbar.dart

@@ -15,6 +15,7 @@ class DraggableScrollbar extends StatefulWidget {
   final EdgeInsetsGeometry padding;
   final int totalCount;
   final int initialScrollIndex;
+  final double bottomSafeArea;
   final int currentFirstIndex;
   final ValueChanged<double> onChange;
   final String Function(int) labelTextBuilder;
@@ -26,6 +27,7 @@ class DraggableScrollbar extends StatefulWidget {
     this.backgroundColor = Colors.white,
     this.drawColor = Colors.grey,
     this.heightScrollThumb = 80.0,
+    this.bottomSafeArea = 120,
     this.padding,
     this.totalCount = 1,
     this.initialScrollIndex = 0,
@@ -49,7 +51,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
 
   double get thumbMin => 0.0;
 
-  double get thumbMax => context.size.height - widget.heightScrollThumb;
+  double get thumbMax =>
+      context.size.height - widget.heightScrollThumb - widget.bottomSafeArea;
 
   AnimationController _thumbAnimationController;
   Animation<double> _thumbAnimation;

+ 23 - 3
lib/ui/huge_listview/huge_listview.dart

@@ -1,6 +1,6 @@
 // @dart=2.9
 
-import 'dart:math' show max;
+import 'dart:math' show max, min;
 
 import 'package:flutter/material.dart';
 import 'package:photos/ui/huge_listview/draggable_scrollbar.dart';
@@ -38,6 +38,10 @@ class HugeListView<T> extends StatefulWidget {
   /// Height of scroll thumb, defaults to 48.
   final double thumbHeight;
 
+  /// Height of bottomSafeArea so that scroll thumb does not become hidden
+  /// or un-clickable due to footer elements. Default value is 120
+  final double bottomSafeArea;
+
   /// Called to build an individual item with the specified [index].
   final HugeListViewItemBuilder<T> itemBuilder;
 
@@ -72,6 +76,7 @@ class HugeListView<T> extends StatefulWidget {
     this.thumbBackgroundColor = Colors.red, // Colors.white,
     this.thumbDrawColor = Colors.yellow, //Colors.grey,
     this.thumbHeight = 48.0,
+    this.bottomSafeArea = 120.0,
     this.isDraggableScrollbarEnabled = true,
     this.thumbPadding,
   }) : super(key: key);
@@ -83,6 +88,7 @@ class HugeListView<T> extends StatefulWidget {
 class HugeListViewState<T> extends State<HugeListView<T>> {
   final scrollKey = GlobalKey<DraggableScrollbarState>();
   final listener = ItemPositionsListener.create();
+  int lastIndexJump = -1;
   dynamic error;
 
   @override
@@ -131,13 +137,27 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
           totalCount: widget.totalCount,
           initialScrollIndex: widget.startIndex,
           onChange: (position) {
-            widget.controller
-                ?.jumpTo(index: (position * widget.totalCount).floor());
+            final int currentIndex = _currentFirst();
+            final int floorIndex = (position * widget.totalCount).floor();
+            final int cielIndex = (position * widget.totalCount).ceil();
+            int nextIndexToJump;
+            if (floorIndex != currentIndex && floorIndex > currentIndex) {
+              nextIndexToJump = floorIndex;
+            } else if (cielIndex != currentIndex && cielIndex < currentIndex) {
+              nextIndexToJump = floorIndex;
+            } else {
+              return;
+            }
+            if (lastIndexJump != nextIndexToJump) {
+              lastIndexJump = nextIndexToJump;
+              widget.controller?.jumpTo(index: nextIndexToJump);
+            }
           },
           labelTextBuilder: widget.labelTextBuilder,
           backgroundColor: widget.thumbBackgroundColor,
           drawColor: widget.thumbDrawColor,
           heightScrollThumb: widget.thumbHeight,
+          bottomSafeArea: widget.bottomSafeArea,
           currentFirstIndex: _currentFirst(),
           isEnabled: widget.isDraggableScrollbarEnabled,
           padding: widget.thumbPadding,

+ 1 - 15
lib/ui/nav_bar.dart

@@ -2,8 +2,6 @@
 
 library google_nav_bar;
 
-import 'dart:async';
-
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
@@ -120,19 +118,7 @@ class _GNavState extends State<GNav> {
                     Colors.transparent,
                 duration: widget.duration ?? const Duration(milliseconds: 500),
                 onPressed: () {
-                  if (!clickable) return;
-                  setState(() {
-                    selectedIndex = widget.tabs.indexOf(t);
-                    clickable = false;
-                  });
-                  widget.onTabChange(selectedIndex);
-
-                  Future.delayed(
-                      widget.duration ?? const Duration(milliseconds: 500), () {
-                    setState(() {
-                      clickable = true;
-                    });
-                  });
+                  widget.onTabChange(widget.tabs.indexOf(t));
                 },
               ),
             )

+ 1 - 1
lib/ui/payment/skip_subscription_widget.dart

@@ -44,7 +44,7 @@ class SkipSubscriptionWidget extends StatelessWidget {
           BillingService.instance
               .verifySubscription(freeProductID, "", paymentProvider: "ente");
         },
-        child: const Text("Continue on free plan"),
+        child: const Text("Continue on free trial"),
       ),
     );
   }

+ 1 - 1
lib/ui/payment/subscription_common_widgets.dart

@@ -94,7 +94,7 @@ class ValidityWidget extends StatelessWidget {
     );
     var message = "Renews on $endDate";
     if (currentSubscription.productID == freeProductID) {
-      message = "Free plan valid till $endDate";
+      message = "Free trial valid till $endDate";
     } else if (currentSubscription.attributes?.isCancelled ?? false) {
       message = "Your subscription will be cancelled on $endDate";
     }

+ 1 - 1
lib/ui/payment/subscription_page.dart

@@ -368,7 +368,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
       planWidgets.add(
         SubscriptionPlanWidget(
           storage: _freePlan.storage,
-          price: "free",
+          price: "Free trial",
           period: "",
           isActive: true,
         ),

+ 1 - 1
lib/ui/payment/subscription_plan_widget.dart

@@ -19,7 +19,7 @@ class SubscriptionPlanWidget extends StatelessWidget {
 
   String _displayPrice() {
     final result = price + (period.isNotEmpty ? " / " + period : "");
-    return result.isNotEmpty ? result : "Trial plan";
+    return price.isNotEmpty ? result : "Free trial";
   }
 
   @override

+ 248 - 153
lib/ui/settings/details_section_widget.dart

@@ -1,10 +1,18 @@
 import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/file_type.dart';
 import 'package:photos/models/user_details.dart';
 import 'package:photos/states/user_details_state.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/ui/payment/subscription.dart';
+import 'package:photos/ui/settings/storage_error_widget.dart';
+import 'package:photos/ui/settings/storage_progress_widget.dart';
 import 'package:photos/utils/data_util.dart';
 
 class DetailsSectionWidget extends StatefulWidget {
@@ -17,6 +25,7 @@ class DetailsSectionWidget extends StatefulWidget {
 class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
   late Image _background;
   final _logger = Logger((_DetailsSectionWidgetState).toString());
+  final ValueNotifier<bool> _isStorageCardPressed = ValueNotifier(false);
 
   @override
   void initState() {
@@ -59,6 +68,12 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
             ),
           );
         },
+        onTapDown: (details) {
+          _isStorageCardPressed.value = true;
+        },
+        onTapUp: (details) {
+          _isStorageCardPressed.value = false;
+        },
         child: containerForUserDetails(inheritedUserDetails),
       );
     }
@@ -68,176 +83,256 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
     InheritedUserDetails inheritedUserDetails,
   ) {
     return ConstrainedBox(
-      constraints: const BoxConstraints(maxWidth: 428, maxHeight: 175),
-      child: Stack(
-        children: [
-          Container(
-            width: double.infinity,
-            color: Colors.transparent,
-            child: AspectRatio(
-              aspectRatio: 2 / 1,
-              child: _background,
+      constraints: const BoxConstraints(maxWidth: 365),
+      child: AspectRatio(
+        aspectRatio: 2 / 1,
+        child: Stack(
+          children: [
+            _background,
+            FutureBuilder(
+              future: inheritedUserDetails.userDetails,
+              builder: (context, snapshot) {
+                if (snapshot.hasData) {
+                  return userDetails(snapshot.data as UserDetails);
+                }
+                if (snapshot.hasError) {
+                  _logger.severe(
+                    'failed to load user details',
+                    snapshot.error,
+                  );
+                  return const StorageErrorWidget();
+                }
+                return const EnteLoadingWidget(color: strokeBaseDark);
+              },
             ),
-          ),
-          FutureBuilder(
-            future: inheritedUserDetails.userDetails,
-            builder: (context, snapshot) {
-              if (snapshot.hasData) {
-                return userDetails(snapshot.data as UserDetails);
-              }
-              if (snapshot.hasError) {
-                _logger.severe('failed to load user details', snapshot.error);
-                return const EnteLoadingWidget();
-              }
-              return const EnteLoadingWidget();
-            },
-          ),
-          const Align(
-            alignment: Alignment.centerRight,
-            child: Icon(
-              Icons.chevron_right,
-              color: Colors.white,
-              size: 24,
+            Align(
+              alignment: Alignment.centerRight,
+              child: Padding(
+                padding: const EdgeInsets.only(right: 4),
+                child: ValueListenableBuilder<bool>(
+                  builder: (BuildContext context, bool value, Widget? child) {
+                    return Icon(
+                      Icons.chevron_right_outlined,
+                      color: value ? strokeMutedDark : strokeBaseDark,
+                    );
+                  },
+                  valueListenable: _isStorageCardPressed,
+                ),
+              ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }
 
   Widget userDetails(UserDetails userDetails) {
+    const hundredMBinBytes = 107374182;
+
+    final isMobileScreenSmall = MediaQuery.of(context).size.width <= 365;
+    final freeSpaceInBytes = userDetails.getFreeStorage();
+    final shouldShowFreeSpaceInMBs = freeSpaceInBytes < hundredMBinBytes;
+
+    final usedSpaceInGB = roundBytesUsedToGBs(
+      userDetails.getFamilyOrPersonalUsage(),
+      userDetails.getFreeStorage(),
+    );
+    final totalStorageInGB =
+        convertBytesToGBs(userDetails.getTotalStorage()).truncate();
+
     return Padding(
-      padding: const EdgeInsets.only(
-        top: 20,
-        bottom: 20,
-        left: 16,
-        right: 16,
+      padding: EdgeInsets.fromLTRB(
+        16,
+        20,
+        16,
+        isMobileScreenSmall
+            ? userDetails.isPartOfFamily()
+                ? 12
+                : 8
+            : userDetails.isPartOfFamily()
+                ? 20
+                : 12,
       ),
-      child: Container(
-        color: Colors.transparent,
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.spaceBetween,
-          children: [
-            Align(
-              alignment: Alignment.topLeft,
-              child: Column(
-                mainAxisAlignment: MainAxisAlignment.start,
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  Text(
-                    "Storage",
-                    style: Theme.of(context).textTheme.subtitle2!.copyWith(
-                          color: Colors.white.withOpacity(0.7),
-                        ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Align(
+            alignment: Alignment.topLeft,
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  isMobileScreenSmall ? "Used space" : "Storage",
+                  style: getEnteTextTheme(context)
+                      .small
+                      .copyWith(color: textMutedDark),
+                ),
+                const SizedBox(height: 2),
+                RichText(
+                  overflow: TextOverflow.ellipsis,
+                  maxLines: 1,
+                  text: TextSpan(
+                    style: getEnteTextTheme(context)
+                        .h3Bold
+                        .copyWith(color: textBaseDark),
+                    children: [
+                      TextSpan(text: usedSpaceInGB.toString()),
+                      TextSpan(text: isMobileScreenSmall ? "/" : " GB of "),
+                      TextSpan(text: totalStorageInGB.toString() + " GB"),
+                      TextSpan(text: isMobileScreenSmall ? "" : " used"),
+                    ],
                   ),
-                  Text(
-                    "${convertBytesToReadableFormat(userDetails.getFreeStorage())} of ${convertBytesToReadableFormat(userDetails.getTotalStorage())} free",
-                    style: Theme.of(context)
-                        .textTheme
-                        .headline5!
-                        .copyWith(color: Colors.white),
+                ),
+              ],
+            ),
+          ),
+          Column(
+            children: [
+              Stack(
+                children: <Widget>[
+                  const StorageProgressWidget(
+                    color:
+                        Color.fromRGBO(255, 255, 255, 0.2), //hardcoded in figma
+                    fractionOfStorage: 1,
                   ),
+                  userDetails.isPartOfFamily()
+                      ? StorageProgressWidget(
+                          color: strokeBaseDark,
+                          fractionOfStorage:
+                              ((userDetails.getFamilyOrPersonalUsage()) /
+                                  userDetails.getTotalStorage()),
+                        )
+                      : const SizedBox.shrink(),
+                  StorageProgressWidget(
+                    color: userDetails.isPartOfFamily()
+                        ? getEnteColorScheme(context).primary300
+                        : strokeBaseDark,
+                    fractionOfStorage:
+                        (userDetails.usage / userDetails.getTotalStorage()),
+                  )
                 ],
               ),
-            ),
-            Column(
-              mainAxisSize: MainAxisSize.max,
-              mainAxisAlignment: MainAxisAlignment.end,
-              crossAxisAlignment: CrossAxisAlignment.end,
-              children: [
-                Stack(
-                  children: <Widget>[
-                    Container(
-                      color: Colors.white.withOpacity(0.2),
-                      width: MediaQuery.of(context).size.width,
-                      height: 4,
-                    ),
-                    Container(
-                      color: Colors.white.withOpacity(0.75),
-                      width: MediaQuery.of(context).size.width *
-                          ((userDetails.getFamilyOrPersonalUsage()) /
-                              userDetails.getTotalStorage()),
-                      height: 4,
-                    ),
-                    Container(
-                      color: Colors.white,
-                      width: MediaQuery.of(context).size.width *
-                          (userDetails.usage / userDetails.getTotalStorage()),
-                      height: 4,
-                    ),
-                  ],
-                ),
-                const Padding(
-                  padding: EdgeInsets.symmetric(vertical: 8),
-                ),
-                Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                  children: [
-                    userDetails.isPartOfFamily()
-                        ? Row(
-                            children: [
-                              Container(
-                                width: 8.71,
-                                height: 8.99,
-                                decoration: const BoxDecoration(
-                                  shape: BoxShape.circle,
-                                  color: Colors.white,
-                                ),
-                              ),
-                              const Padding(
-                                padding: EdgeInsets.only(right: 4),
-                              ),
-                              Text(
-                                "You",
-                                style: Theme.of(context)
-                                    .textTheme
-                                    .bodyText1!
-                                    .copyWith(
-                                      color: Colors.white,
-                                      fontSize: 12,
-                                    ),
-                              ),
-                              const Padding(
-                                padding: EdgeInsets.only(right: 12),
-                              ),
-                              Container(
-                                width: 8.71,
-                                height: 8.99,
-                                decoration: BoxDecoration(
-                                  shape: BoxShape.circle,
-                                  color: Colors.white.withOpacity(0.75),
-                                ),
-                              ),
-                              const Padding(
-                                padding: EdgeInsets.only(right: 4),
+              const SizedBox(height: 12),
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  userDetails.isPartOfFamily()
+                      ? Row(
+                          children: [
+                            Container(
+                              width: 8.71,
+                              height: 8.99,
+                              decoration: BoxDecoration(
+                                shape: BoxShape.circle,
+                                color: getEnteColorScheme(context).primary300,
                               ),
-                              Text(
-                                "Family",
-                                style: Theme.of(context)
-                                    .textTheme
-                                    .bodyText1!
-                                    .copyWith(
-                                      color: Colors.white,
-                                      fontSize: 12,
-                                    ),
+                            ),
+                            const SizedBox(width: 4),
+                            Text(
+                              "You",
+                              style: getEnteTextTheme(context)
+                                  .miniBold
+                                  .copyWith(color: textBaseDark),
+                            ),
+                            const SizedBox(width: 12),
+                            Container(
+                              width: 8.71,
+                              height: 8.99,
+                              decoration: const BoxDecoration(
+                                shape: BoxShape.circle,
+                                color: textBaseDark,
                               ),
-                            ],
-                          )
-                        : Text(
-                            "${convertBytesToReadableFormat(userDetails.getFamilyOrPersonalUsage())} used",
-                            style:
-                                Theme.of(context).textTheme.bodyText1!.copyWith(
-                                      color: Colors.white,
-                                      fontSize: 12,
-                                    ),
+                            ),
+                            const SizedBox(width: 4),
+                            Text(
+                              "Family",
+                              style: getEnteTextTheme(context)
+                                  .miniBold
+                                  .copyWith(color: textBaseDark),
+                            ),
+                          ],
+                        )
+                      : FutureBuilder(
+                          future: FilesDB.instance.fetchFilesCountbyType(
+                            Configuration.instance.getUserID(),
                           ),
-                  ],
-                ),
-              ],
-            )
-          ],
-        ),
+                          builder: (context, snapshot) {
+                            if (snapshot.hasData) {
+                              final filesCount = FilesCount(
+                                snapshot.data as Map<FileType, int>,
+                              );
+                              return Column(
+                                crossAxisAlignment: CrossAxisAlignment.start,
+                                children: [
+                                  Text(
+                                    "${NumberFormat().format(filesCount.photos)} photos",
+                                    style: getEnteTextTheme(context)
+                                        .mini
+                                        .copyWith(color: textBaseDark),
+                                  ),
+                                  Text(
+                                    "${NumberFormat().format(filesCount.videos)} videos",
+                                    style: getEnteTextTheme(context)
+                                        .mini
+                                        .copyWith(color: textBaseDark),
+                                  ),
+                                ],
+                              );
+                            } else if (snapshot.hasError) {
+                              _logger.severe(
+                                'Error fetching photo and video count',
+                                snapshot.error,
+                              );
+                              return const SizedBox.shrink();
+                            } else {
+                              return const EnteLoadingWidget(
+                                color: strokeBaseDark,
+                              );
+                            }
+                          },
+                        ),
+                  RichText(
+                    text: TextSpan(
+                      style: getEnteTextTheme(context)
+                          .mini
+                          .copyWith(color: textFaintDark),
+                      children: [
+                        TextSpan(
+                          text:
+                              "${shouldShowFreeSpaceInMBs ? convertBytesToMBs(freeSpaceInBytes) : _roundedFreeSpace(totalStorageInGB, usedSpaceInGB)}",
+                        ),
+                        TextSpan(
+                          text: shouldShowFreeSpaceInMBs
+                              ? " MB free"
+                              : " GB free",
+                        )
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          )
+        ],
       ),
     );
   }
+
+  num _roundedFreeSpace(num totalStorageInGB, num usedSpaceInGB) {
+    int fractionDigits;
+    //subtracting usedSpace from totalStorage in GB instead of converting from bytes so that free space and used space adds up in the UI
+    final freeSpace = totalStorageInGB - usedSpaceInGB;
+    //show one decimal place if free space is less than 10GB
+    if (freeSpace < 10) {
+      fractionDigits = 1;
+    } else {
+      fractionDigits = 0;
+    }
+    //omit decimal if decimal is 0
+    if (fractionDigits == 1 && freeSpace.remainder(1) == 0) {
+      fractionDigits = 0;
+    }
+    return num.parse(freeSpace.toStringAsFixed(fractionDigits));
+  }
 }

+ 13 - 19
lib/ui/settings/settings_title_bar_widget.dart

@@ -1,8 +1,10 @@
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/file_type.dart';
 import 'package:photos/models/user_details.dart';
-import 'package:photos/states/user_details_state.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 
@@ -27,30 +29,22 @@ class SettingsTitleBarWidget extends StatelessWidget {
               icon: const Icon(Icons.keyboard_double_arrow_left_outlined),
             ),
             FutureBuilder(
-              future: InheritedUserDetails.of(context)?.userDetails,
+              future: FilesDB.instance
+                  .fetchFilesCountbyType(Configuration.instance.getUserID()),
               builder: (context, snapshot) {
-                if (InheritedUserDetails.of(context) == null) {
-                  logger.severe(
-                    (InheritedUserDetails).toString() +
-                        ' not found before ' +
-                        (SettingsTitleBarWidget).toString() +
-                        ' on tree',
-                  );
-                  throw Error();
-                }
                 if (snapshot.hasData) {
-                  final userDetails = snapshot.data as UserDetails;
+                  final totalFiles =
+                      FilesCount(snapshot.data as Map<FileType, int>).total;
                   return Text(
-                    "${NumberFormat().format(userDetails.fileCount)} memories",
+                    totalFiles == 0
+                        ? "No memories yet"
+                        : "${NumberFormat().format(totalFiles)} memories",
                     style: getEnteTextTheme(context).largeBold,
                   );
+                } else if (snapshot.hasError) {
+                  logger.severe('failed to fetch filesCount');
                 }
-                if (snapshot.hasError) {
-                  logger.severe('failed to load user details');
-                  return const EnteLoadingWidget();
-                } else {
-                  return const EnteLoadingWidget();
-                }
+                return const EnteLoadingWidget();
               },
             )
           ],

+ 31 - 0
lib/ui/settings/storage_error_widget.dart

@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class StorageErrorWidget extends StatelessWidget {
+  const StorageErrorWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(12),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          const Icon(
+            Icons.error_outline_outlined,
+            color: strokeBaseDark,
+          ),
+          const SizedBox(height: 8),
+          Text(
+            "Your storage details could not be fetched",
+            style: getEnteTextTheme(context).small.copyWith(
+                  color: textMutedDark,
+                ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 27 - 0
lib/ui/settings/storage_progress_widget.dart

@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+
+class StorageProgressWidget extends StatelessWidget {
+  final Color color;
+  final double fractionOfStorage;
+  const StorageProgressWidget({
+    required this.color,
+    required this.fractionOfStorage,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (context, constrains) {
+        return Container(
+          decoration: BoxDecoration(
+            borderRadius: BorderRadius.circular(2),
+            color: color,
+          ),
+          width: constrains.maxWidth * fractionOfStorage,
+          height: 4,
+        );
+      },
+    );
+  }
+}

+ 22 - 2
lib/ui/viewer/gallery/gallery.dart

@@ -9,6 +9,7 @@ import 'package:photos/core/event_bus.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/event.dart';
 import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/events/tab_changed_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_load_result.dart';
 import 'package:photos/models/selected_files.dart';
@@ -38,6 +39,7 @@ class Gallery extends StatefulWidget {
   final Widget footer;
   final bool smallerTodayFont;
   final String albumName;
+  final double scrollBottomSafeArea;
 
   const Gallery({
     @required this.asyncLoader,
@@ -49,6 +51,7 @@ class Gallery extends StatefulWidget {
     this.removalEventTypes = const {},
     this.header,
     this.footer = const SizedBox(height: 120),
+    this.scrollBottomSafeArea = 120.0,
     this.smallerTodayFont = false,
     this.albumName = '',
     Key key,
@@ -68,12 +71,16 @@ class _GalleryState extends State<Gallery> {
   Logger _logger;
   List<List<File>> _collatedFiles = [];
   bool _hasLoadedFiles = false;
+  ItemScrollController _itemScroller;
   StreamSubscription<FilesUpdatedEvent> _reloadEventSubscription;
+  StreamSubscription<TabDoubleTapEvent> _tabDoubleTapEvent;
   final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
 
   @override
   void initState() {
     _logger = Logger("Gallery_" + widget.tagPrefix);
+    _itemScroller = ItemScrollController();
+
     _logger.info("initState");
     if (widget.reloadEvent != null) {
       _reloadEventSubscription = widget.reloadEvent.listen((event) async {
@@ -82,6 +89,17 @@ class _GalleryState extends State<Gallery> {
         _onFilesLoaded(result.files);
       });
     }
+    _tabDoubleTapEvent =
+        Bus.instance.on<TabDoubleTapEvent>().listen((event) async {
+      // todo: Assign ID to Gallery and fire generic event with ID &
+      //  target index/date
+      if (mounted && event.selectedIndex == 0) {
+        _itemScroller.scrollTo(
+          index: 0,
+          duration: const Duration(milliseconds: 150),
+        );
+      }
+    });
     if (widget.forceReloadEvents != null) {
       for (final event in widget.forceReloadEvents) {
         _forceReloadEventSubscriptions.add(
@@ -159,6 +177,7 @@ class _GalleryState extends State<Gallery> {
   @override
   void dispose() {
     _reloadEventSubscription?.cancel();
+    _tabDoubleTapEvent?.cancel();
     for (final subscription in _forceReloadEventSubscriptions) {
       subscription.cancel();
     }
@@ -177,10 +196,10 @@ class _GalleryState extends State<Gallery> {
   Widget _getListView() {
     return HugeListView<List<File>>(
       key: _hugeListViewKey,
-      controller: ItemScrollController(),
+      controller: _itemScroller,
       startIndex: 0,
       totalCount: _collatedFiles.length,
-      isDraggableScrollbarEnabled: _collatedFiles.length > 30,
+      isDraggableScrollbarEnabled: _collatedFiles.length > 10,
       waitBuilder: (_) {
         return const EnteLoadingWidget();
       },
@@ -239,6 +258,7 @@ class _GalleryState extends State<Gallery> {
       thumbPadding: widget.header != null
           ? const EdgeInsets.only(top: 60)
           : const EdgeInsets.all(0),
+      bottomSafeArea: widget.scrollBottomSafeArea,
       firstShown: (int firstIndex) {
         Bus.instance
             .fire(GalleryIndexUpdatedEvent(widget.tagPrefix, firstIndex));

+ 20 - 6
lib/utils/data_util.dart

@@ -1,11 +1,5 @@
 import 'dart:math';
 
-double convertBytesToGBs(final int bytes, {int precision = 2}) {
-  return double.parse(
-    (bytes / (1024 * 1024 * 1024)).toStringAsFixed(precision),
-  );
-}
-
 final storageUnits = ["bytes", "KB", "MB", "GB"];
 
 String convertBytesToReadableFormat(int bytes) {
@@ -24,3 +18,23 @@ String formatBytes(int bytes, [int decimals = 2]) {
   final int i = (log(bytes) / log(k)).floor();
   return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + storageUnits[i];
 }
+
+//shows 1st decimal only if less than 10GB & omits decimal if decimal is 0
+num roundBytesUsedToGBs(int usedBytes, int freeSpace) {
+  const tenGBinBytes = 10737418240;
+  num bytesInGB = convertBytesToGBs(usedBytes);
+  if ((usedBytes >= tenGBinBytes && freeSpace >= tenGBinBytes) ||
+      bytesInGB % 1 == 0) {
+    bytesInGB = bytesInGB.truncate();
+  }
+  return bytesInGB;
+}
+
+//Eg: 0.3 GB, 11.0 GB, 532.3 GB
+num convertBytesToGBs(int bytes) {
+  return num.parse((bytes / (pow(1024, 3))).toStringAsFixed(1));
+}
+
+int convertBytesToMBs(int bytes) {
+  return (bytes / pow(1024, 2)).round();
+}

+ 3 - 3
pubspec.lock

@@ -411,7 +411,7 @@ packages:
       name: flutter_inappwebview
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "5.4.3+7"
+    version: "5.7.1"
   flutter_keyboard_visibility:
     dependency: transitive
     description:
@@ -927,7 +927,7 @@ packages:
       name: photo_manager
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.4"
+    version: "2.4.1"
   photo_view:
     dependency: "direct main"
     description:
@@ -1004,7 +1004,7 @@ packages:
       name: scrollable_positioned_list
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.2.3"
+    version: "0.3.5"
   sentry:
     dependency: "direct main"
     description:

+ 3 - 3
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
-version: 0.6.47+377
+version: 0.6.51+381
 
 environment:
   sdk: '>=2.17.0 <3.0.0'
@@ -91,13 +91,13 @@ dependencies:
   path: #dart
   path_provider: ^2.0.1
   pedantic: ^1.9.2
-  photo_manager: 2.1.4
+  photo_manager: ^2.4.1
   photo_view: ^0.14.0
   pinput: ^1.2.2
   provider: ^6.0.0
   quiver: ^3.0.1
   receive_sharing_intent: ^1.4.5
-  scrollable_positioned_list: ^0.2.2
+  scrollable_positioned_list: ^0.3.5
   sentry: ^6.12.1
   sentry_flutter: ^6.12.1
   share_plus: ^4.0.10