Переглянути джерело

Merge branch 'master' into select-photos-of-day

ashilkn 2 роки тому
батько
коміт
0b3096c7d5

+ 2 - 2
.github/workflows/code_quality.yml

@@ -29,5 +29,5 @@ jobs:
 
 
       - name: Run Linter
       - name: Run Linter
         run: flutter analyze --no-fatal-infos
         run: flutter analyze --no-fatal-infos
-#      - name: Run Test :sed:
-#        run: flutter test
+      - name: Run Test
+        run: flutter test

+ 1 - 1
lib/core/constants.dart

@@ -11,7 +11,7 @@ const String sentryTunnel = "https://sentry-reporter.ente.io";
 const String roadmapURL = "https://roadmap.ente.io";
 const String roadmapURL = "https://roadmap.ente.io";
 const int microSecondsInDay = 86400000000;
 const int microSecondsInDay = 86400000000;
 const int android11SDKINT = 30;
 const int android11SDKINT = 30;
-const int jan011991Time = 31580904000000;
+const int jan011981Time = 347135400000000;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 
 

+ 28 - 12
lib/models/file.dart

@@ -74,24 +74,40 @@ class File extends EnteFile {
     file.deviceFolder = pathName;
     file.deviceFolder = pathName;
     file.location = Location(asset.latitude, asset.longitude);
     file.location = Location(asset.latitude, asset.longitude);
     file.fileType = _fileTypeFromAsset(asset);
     file.fileType = _fileTypeFromAsset(asset);
-    file.creationTime = asset.createDateTime.microsecondsSinceEpoch;
-    if (file.creationTime == null || (file.creationTime! <= jan011991Time)) {
-      try {
-        final parsedDateTime =
-            parseDateFromFileName(basenameWithoutExtension(file.title ?? ""));
-
-        file.creationTime = parsedDateTime?.microsecondsSinceEpoch ??
-            asset.modifiedDateTime.microsecondsSinceEpoch;
-      } catch (e) {
-        file.creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
-      }
-    }
+    file.creationTime = parseFileCreationTime(file.title, asset);
     file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
     file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
     file.fileSubType = asset.subtype;
     file.fileSubType = asset.subtype;
     file.metadataVersion = kCurrentMetadataVersion;
     file.metadataVersion = kCurrentMetadataVersion;
     return file;
     return file;
   }
   }
 
 
+  static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
+    int creationTime = asset.createDateTime.microsecondsSinceEpoch;
+    if (creationTime >= jan011981Time) {
+      // assuming that fileSystem is returning correct creationTime.
+      // During upload, this might get overridden with exif Creation time
+      return creationTime;
+    } else {
+      if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
+        creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
+      } else {
+        creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
+      }
+      try {
+        final parsedDateTime = parseDateTimeFromFileNameV2(
+          basenameWithoutExtension(fileTitle ?? ""),
+        );
+        if (parsedDateTime != null) {
+          creationTime = parsedDateTime.microsecondsSinceEpoch;
+        }
+      } catch (e) {
+        // ignore
+      }
+    }
+
+    return creationTime;
+  }
+
   static FileType _fileTypeFromAsset(AssetEntity asset) {
   static FileType _fileTypeFromAsset(AssetEntity asset) {
     FileType type = FileType.image;
     FileType type = FileType.image;
     switch (asset.type) {
     switch (asset.type) {

+ 16 - 0
lib/services/update_service.dart

@@ -15,6 +15,8 @@ class UpdateService {
 
 
   static final UpdateService instance = UpdateService._privateConstructor();
   static final UpdateService instance = UpdateService._privateConstructor();
   static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
   static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
+  static const changeLogVersionKey = "update_change_log_key";
+  static const currentChangeLogVersion = 1;
 
 
   LatestVersionInfo _latestVersion;
   LatestVersionInfo _latestVersion;
   final _logger = Logger("UpdateService");
   final _logger = Logger("UpdateService");
@@ -26,6 +28,20 @@ class UpdateService {
     _prefs = await SharedPreferences.getInstance();
     _prefs = await SharedPreferences.getInstance();
   }
   }
 
 
+  Future<bool> showChangeLog() async {
+    // fetch the change log version which was last shown to user.
+    final lastShownAtVersion = _prefs.getInt(changeLogVersionKey) ?? 0;
+    return lastShownAtVersion < currentChangeLogVersion;
+  }
+
+  Future<bool> hideChangeLog() async {
+    return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
+  }
+
+  Future<bool> resetChangeLog() {
+    return _prefs.remove(changeLogVersionKey);
+  }
+
   Future<bool> shouldUpdate() async {
   Future<bool> shouldUpdate() async {
     if (!isIndependent()) {
     if (!isIndependent()) {
       return false;
       return false;

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

@@ -6,6 +6,7 @@ import 'package:dots_indicator/dots_indicator.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
+import 'package:photos/services/update_service.dart';
 import 'package:photos/ui/account/email_entry_page.dart';
 import 'package:photos/ui/account/email_entry_page.dart';
 import 'package:photos/ui/account/login_page.dart';
 import 'package:photos/ui/account/login_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
@@ -152,6 +153,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
   }
   }
 
 
   void _navigateToSignUpPage() {
   void _navigateToSignUpPage() {
+    UpdateService.instance.hideChangeLog().ignore();
     Widget page;
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const EmailEntryPage();
       page = const EmailEntryPage();
@@ -178,6 +180,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
   }
   }
 
 
   void _navigateToSignInPage() {
   void _navigateToSignInPage() {
+    UpdateService.instance.hideChangeLog().ignore();
     Widget page;
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const LoginPage();
       page = const LoginPage();

+ 41 - 0
lib/ui/home_widget.dart

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
 import 'package:move_to_background/move_to_background.dart';
 import 'package:move_to_background/move_to_background.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
@@ -26,6 +27,7 @@ import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/states/user_details_state.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/theme/ente_theme.dart';
 import 'package:photos/ui/collections_gallery_widget.dart';
 import 'package:photos/ui/collections_gallery_widget.dart';
 import 'package:photos/ui/common/bottom_shadow.dart';
 import 'package:photos/ui/common/bottom_shadow.dart';
@@ -39,6 +41,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
 import 'package:photos/ui/home/preserve_footer_widget.dart';
 import 'package:photos/ui/home/preserve_footer_widget.dart';
 import 'package:photos/ui/home/start_backup_hook_widget.dart';
 import 'package:photos/ui/home/start_backup_hook_widget.dart';
 import 'package:photos/ui/loading_photos_widget.dart';
 import 'package:photos/ui/loading_photos_widget.dart';
+import 'package:photos/ui/notification/update/change_log_page.dart';
 import 'package:photos/ui/settings/app_update_dialog.dart';
 import 'package:photos/ui/settings/app_update_dialog.dart';
 import 'package:photos/ui/settings_page.dart';
 import 'package:photos/ui/settings_page.dart';
 import 'package:photos/ui/shared_collections_gallery.dart';
 import 'package:photos/ui/shared_collections_gallery.dart';
@@ -172,6 +175,15 @@ class _HomeWidgetState extends State<HomeWidget> {
     });
     });
     // For sharing images coming from outside the app while the app is in the memory
     // For sharing images coming from outside the app while the app is in the memory
     _initMediaShareSubscription();
     _initMediaShareSubscription();
+    WidgetsBinding.instance.addPostFrameCallback(
+      (_) => Future.delayed(
+        const Duration(seconds: 1),
+        () => {
+          if (mounted) {showChangeLog(context)}
+        },
+      ),
+    );
+
     super.initState();
     super.initState();
   }
   }
 
 
@@ -414,4 +426,33 @@ class _HomeWidgetState extends State<HomeWidget> {
     final ott = Uri.parse(link).queryParameters["ott"];
     final ott = Uri.parse(link).queryParameters["ott"];
     UserService.instance.verifyEmail(context, ott);
     UserService.instance.verifyEmail(context, ott);
   }
   }
+
+  showChangeLog(BuildContext context) async {
+    final bool show = await UpdateService.instance.showChangeLog();
+    if (!show || !Configuration.instance.isLoggedIn()) {
+      return;
+    }
+    final colorScheme = getEnteColorScheme(context);
+    await showBarModalBottomSheet(
+      topControl: const SizedBox.shrink(),
+      shape: const RoundedRectangleBorder(
+        borderRadius: BorderRadius.only(
+          topLeft: Radius.circular(5),
+          topRight: Radius.circular(5),
+        ),
+      ),
+      backgroundColor: colorScheme.backgroundElevated,
+      barrierColor: backdropMutedDark,
+      context: context,
+      builder: (BuildContext context) {
+        return Padding(
+          padding:
+              EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+          child: const ChangeLogPage(),
+        );
+      },
+    );
+    // Do not show change dialog again
+    UpdateService.instance.hideChangeLog().ignore();
+  }
 }
 }

+ 52 - 0
lib/ui/notification/update/change_log_entry.dart

@@ -0,0 +1,52 @@
+import 'package:flutter/widgets.dart';
+import 'package:photos/theme/ente_theme.dart';
+
+class ChangeLogEntry {
+  final bool isFeature;
+  final String title;
+  final String description;
+
+  ChangeLogEntry(this.title, this.description, {this.isFeature = true});
+}
+
+class ChangeLogEntryWidget extends StatelessWidget {
+  final ChangeLogEntry entry;
+
+  const ChangeLogEntryWidget({
+    super.key,
+    required this.entry,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final enteTheme = getEnteTextTheme(context);
+    final colorScheme = getEnteColorScheme(context);
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          entry.title,
+          textAlign: TextAlign.left,
+          style: enteTheme.largeBold.copyWith(
+            color: entry.isFeature
+                ? colorScheme.primary700
+                : colorScheme.textMuted,
+          ),
+        ),
+        const SizedBox(
+          height: 18,
+        ),
+        Text(
+          entry.description,
+          textAlign: TextAlign.left,
+          style: enteTheme.body.copyWith(
+            color: colorScheme.textMuted,
+          ),
+        ),
+        const SizedBox(
+          height: 18,
+        ),
+      ],
+    );
+  }
+}

+ 199 - 0
lib/ui/notification/update/change_log_page.dart

@@ -0,0 +1,199 @@
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:photos/services/update_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/common/gradient_button.dart';
+import 'package:photos/ui/common/web_page.dart';
+import 'package:photos/ui/components/divider_widget.dart';
+import 'package:photos/ui/components/title_bar_title_widget.dart';
+import 'package:photos/ui/notification/update/change_log_entry.dart';
+
+class ChangeLogPage extends StatefulWidget {
+  const ChangeLogPage({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<ChangeLogPage> createState() => _ChangeLogPageState();
+}
+
+class _ChangeLogPageState extends State<ChangeLogPage> {
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final enteColorScheme = getEnteColorScheme(context);
+    final enteTextTheme = getEnteTextTheme(context);
+    return Scaffold(
+      appBar: null,
+      body: Container(
+        color: enteColorScheme.backgroundElevated,
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            const SizedBox(
+              height: 36,
+            ),
+            SafeArea(
+              child: Container(
+                alignment: Alignment.centerLeft,
+                child: const Padding(
+                  padding: EdgeInsets.symmetric(horizontal: 16.0),
+                  child: TitleBarTitleWidget(
+                    title: "What's new",
+                  ),
+                ),
+              ),
+            ),
+            Expanded(child: _getChangeLog()),
+            const DividerWidget(
+              dividerType: DividerType.solid,
+            ),
+            SafeArea(
+              child: Padding(
+                padding: const EdgeInsets.only(
+                  left: 16.0,
+                  right: 16,
+                  top: 16,
+                  bottom: 8,
+                ),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    SizedBox(
+                      width: double.infinity,
+                      child: GradientButton(
+                        onTap: () async {
+                          await UpdateService.instance.hideChangeLog();
+                          Navigator.of(context).pop();
+                        },
+                        text: "Let's go",
+                      ),
+                    ),
+                    Padding(
+                      padding: const EdgeInsets.only(
+                        left: 12,
+                        top: 12,
+                        right: 12,
+                        bottom: 6,
+                      ),
+                      child: RichText(
+                        textAlign: TextAlign.center,
+                        text: TextSpan(
+                          children: [
+                            const TextSpan(
+                              text: "If you like ente, ",
+                            ),
+                            TextSpan(
+                              text: "let others know",
+                              style: enteTextTheme.small.copyWith(
+                                color: enteColorScheme.primary700,
+                                decoration: TextDecoration.underline,
+                              ),
+                              recognizer: TapGestureRecognizer()
+                                ..onTap = () {
+                                  // Single tapped.
+                                  Navigator.of(context).push(
+                                    MaterialPageRoute(
+                                      builder: (BuildContext context) {
+                                        return const WebPage(
+                                          "Spread the word",
+                                          "https://ente.io/share/",
+                                        );
+                                      },
+                                    ),
+                                  );
+                                },
+                            ),
+                          ],
+                          style: enteTextTheme.small,
+                        ),
+                      ),
+                    ),
+                    const SizedBox(height: 8),
+                  ],
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _getChangeLog() {
+    final scrollController = ScrollController();
+    final List<ChangeLogEntry> items = [];
+    items.add(
+      ChangeLogEntry(
+        "Hide your photos!",
+        "On popular demand, "
+            "ente now supports photos that are hidden behind a lock.\n\nThis "
+            "is in "
+            "addition to the existing functionality to archive your photos so "
+            "that they do not show in your timeline (but are otherwise visible)"
+            ".",
+      ),
+    );
+    items.add(
+      ChangeLogEntry(
+        '''Add a description to your photos''',
+        "You can now add a caption / description to your photos and videos"
+            ".These will show up on the photo view.\n\nTo add a description, tap on the info icon to view the photo details and enter your text.",
+      ),
+    );
+
+    items.add(
+      ChangeLogEntry(
+        '''And search photos descriptions too''',
+        "Yes, it doesn't end there! You can also search your photos using "
+            "their descriptions.\n\nThis allows you to, for example, tag your"
+            " photos and quickly search for them.",
+      ),
+    );
+    if (Platform.isIOS) {
+      items.add(
+        ChangeLogEntry(
+          '''Save live photos''',
+          "There are some small fixes, including an enhancement to download and save live photos.",
+          isFeature: false,
+        ),
+      );
+    } else {
+      items.add(
+        ChangeLogEntry(
+          '''Better import of WhatsApp photos''',
+          "There are some small fixes, including an enhancement to use the creation time for photos imported from WhatsApp.",
+          isFeature: false,
+        ),
+      );
+    }
+
+    return Container(
+      padding: const EdgeInsets.only(left: 16),
+      child: Scrollbar(
+        controller: scrollController,
+        thumbVisibility: true,
+        thickness: 2.0,
+        child: ListView.builder(
+          itemBuilder: (context, index) {
+            return Padding(
+              padding: const EdgeInsets.only(right: 16.0),
+              child: ChangeLogEntryWidget(entry: items[index]),
+            );
+          },
+          physics: const ClampingScrollPhysics(),
+          itemCount: items.length,
+          shrinkWrap: true,
+        ),
+      ),
+    );
+  }
+}

+ 61 - 18
lib/ui/settings/app_update_dialog.dart

@@ -7,6 +7,8 @@ import 'package:photos/core/configuration.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:url_launcher/url_launcher_string.dart';
 
 
 class AppUpdateDialog extends StatefulWidget {
 class AppUpdateDialog extends StatefulWidget {
   final LatestVersionInfo latestVersionInfo;
   final LatestVersionInfo latestVersionInfo;
@@ -21,11 +23,30 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final List<Widget> changelog = [];
     final List<Widget> changelog = [];
+    final enteTextTheme = getEnteTextTheme(context);
+    final enteColor = getEnteColorScheme(context);
     for (final log in widget.latestVersionInfo.changelog) {
     for (final log in widget.latestVersionInfo.changelog) {
       changelog.add(
       changelog.add(
         Padding(
         Padding(
-          padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
-          child: Text("- " + log, style: Theme.of(context).textTheme.caption),
+          padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                "- ",
+                style: enteTextTheme.small.copyWith(color: enteColor.textMuted),
+              ),
+              Flexible(
+                child: Text(
+                  log,
+                  softWrap: true,
+                  style:
+                      enteTextTheme.small.copyWith(color: enteColor.textMuted),
+                ),
+              )
+            ],
+          ),
         ),
         ),
       );
       );
     }
     }
@@ -34,20 +55,10 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
       mainAxisSize: MainAxisSize.min,
       mainAxisSize: MainAxisSize.min,
       children: [
       children: [
         Text(
         Text(
-          widget.latestVersionInfo.name,
-          style: const TextStyle(
-            fontSize: 20,
-            fontWeight: FontWeight.bold,
-          ),
+          "A new version of ente is available.",
+          style: enteTextTheme.body.copyWith(color: enteColor.textMuted),
         ),
         ),
         const Padding(padding: EdgeInsets.all(8)),
         const Padding(padding: EdgeInsets.all(8)),
-        const Text(
-          "Changelog",
-          style: TextStyle(
-            fontSize: 18,
-          ),
-        ),
-        const Padding(padding: EdgeInsets.all(4)),
         Column(
         Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           crossAxisAlignment: CrossAxisAlignment.start,
           children: changelog,
           children: changelog,
@@ -55,12 +66,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
         const Padding(padding: EdgeInsets.all(8)),
         const Padding(padding: EdgeInsets.all(8)),
         SizedBox(
         SizedBox(
           width: double.infinity,
           width: double.infinity,
-          height: 64,
+          height: 56,
           child: OutlinedButton(
           child: OutlinedButton(
             style: Theme.of(context).outlinedButtonTheme.style.copyWith(
             style: Theme.of(context).outlinedButtonTheme.style.copyWith(
               textStyle: MaterialStateProperty.resolveWith<TextStyle>(
               textStyle: MaterialStateProperty.resolveWith<TextStyle>(
                 (Set<MaterialState> states) {
                 (Set<MaterialState> states) {
-                  return Theme.of(context).textTheme.subtitle1;
+                  return enteTextTheme.bodyBold;
                 },
                 },
               ),
               ),
             ),
             ),
@@ -79,6 +90,22 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
             ),
             ),
           ),
           ),
         ),
         ),
+        const Padding(padding: EdgeInsets.all(8)),
+        Center(
+          child: InkWell(
+            child: Text(
+              "Install manually",
+              style: Theme.of(context)
+                  .textTheme
+                  .caption
+                  .copyWith(decoration: TextDecoration.underline),
+            ),
+            onTap: () => launchUrlString(
+              widget.latestVersionInfo.url,
+              mode: LaunchMode.externalApplication,
+            ),
+          ),
+        )
       ],
       ],
     );
     );
     final shouldForceUpdate =
     final shouldForceUpdate =
@@ -86,8 +113,24 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
     return WillPopScope(
     return WillPopScope(
       onWillPop: () async => !shouldForceUpdate,
       onWillPop: () async => !shouldForceUpdate,
       child: AlertDialog(
       child: AlertDialog(
-        title: Text(
-          shouldForceUpdate ? "Critical update available" : "Update available",
+        title: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Icon(
+              Icons.auto_awesome_outlined,
+              size: 48,
+              color: enteColor.strokeMuted,
+            ),
+            const SizedBox(
+              height: 16,
+            ),
+            Text(
+              shouldForceUpdate
+                  ? "Critical update available"
+                  : "Update available",
+              style: enteTextTheme.h3Bold,
+            ),
+          ],
         ),
         ),
         content: content,
         content: content,
       ),
       ),

+ 2 - 0
lib/ui/settings/debug_section_widget.dart

@@ -6,6 +6,7 @@ import 'package:photos/core/configuration.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/sync_service.dart';
+import 'package:photos/services/update_service.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/captioned_text_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
 import 'package:photos/ui/components/expandable_menu_item_widget.dart';
@@ -37,6 +38,7 @@ class DebugSectionWidget extends StatelessWidget {
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
+            await UpdateService.instance.resetChangeLog();
             _showKeyAttributesDialog(context);
             _showKeyAttributesDialog(context);
           },
           },
         ),
         ),

+ 43 - 0
lib/utils/date_time_util.dart

@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
 
 
@@ -268,6 +269,7 @@ bool isValidDate({
   return true;
   return true;
 }
 }
 
 
+@Deprecated("Use parseDateTimeV2 ")
 DateTime? parseDateFromFileName(String fileName) {
 DateTime? parseDateFromFileName(String fileName) {
   if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
   if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
 // Whatsapp media files
 // Whatsapp media files
@@ -287,3 +289,44 @@ DateTime? parseDateFromFileName(String fileName) {
     );
     );
   }
   }
 }
 }
+
+final RegExp exp = RegExp('[A-Za-z]');
+
+DateTime? parseDateTimeFromFileNameV2(String fileName) {
+  String val = fileName.replaceAll(exp, '');
+  if (val.isNotEmpty && !isNumeric(val[0])) {
+    val = val.substring(1, val.length);
+  }
+  if (val.isNotEmpty && !isNumeric(val[val.length - 1])) {
+    val = val.substring(0, val.length - 1);
+  }
+  final int countOfHyphen = val.split("-").length - 1;
+  final int countUnderScore = val.split("_").length - 1;
+  String valForParser = val;
+  if (countOfHyphen == 1) {
+    valForParser = val.replaceAll("-", "T");
+  } else if (countUnderScore == 1 || countUnderScore == 2) {
+    valForParser = val.replaceFirst("_", "T");
+    if (countUnderScore == 2) {
+      valForParser = valForParser.split("_")[0];
+    }
+  } else if (countOfHyphen == 2) {
+    valForParser = val.replaceAll(".", ":");
+  } else if (countOfHyphen == 6) {
+    final splits = val.split("-");
+    valForParser =
+        "${splits[0]}${splits[1]}${splits[2]}T${splits[3]}${splits[4]}${splits[5]}";
+  }
+  final result = DateTime.tryParse(valForParser);
+  if (kDebugMode && result == null) {
+    debugPrint("Failed to parse $fileName dateTime from $valForParser");
+  }
+  return result;
+}
+
+bool isNumeric(String? s) {
+  if (s == null) {
+    return false;
+  }
+  return double.tryParse(s) != null;
+}

+ 1 - 1
lib/utils/share_util.dart

@@ -99,7 +99,7 @@ Future<List<File>> convertIncomingSharedMediaToFile(
     }
     }
     if (enteFile.creationTime == null || enteFile.creationTime == 0) {
     if (enteFile.creationTime == null || enteFile.creationTime == 0) {
       final parsedDateTime =
       final parsedDateTime =
-          parseDateFromFileName(basenameWithoutExtension(media.path));
+          parseDateTimeFromFileNameV2(basenameWithoutExtension(media.path));
       if (parsedDateTime != null) {
       if (parsedDateTime != null) {
         enteFile.creationTime = parsedDateTime.microsecondsSinceEpoch;
         enteFile.creationTime = parsedDateTime.microsecondsSinceEpoch;
       } else {
       } else {

+ 161 - 0
pubspec.lock

@@ -1,6 +1,13 @@
 # Generated by pub
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
 packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "47.0.0"
   adaptive_theme:
   adaptive_theme:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -8,6 +15,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "3.1.1"
     version: "3.1.1"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.7.0"
   animate_do:
   animate_do:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -162,6 +176,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "3.0.2"
     version: "3.0.2"
+  coverage:
+    dependency: transitive
+    description:
+      name: coverage
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.2"
   crypto:
   crypto:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -546,6 +567,20 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "8.0.9"
     version: "8.0.9"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
   google_nav_bar:
   google_nav_bar:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -581,6 +616,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "2.0.3"
     version: "2.0.3"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.2.1"
   http_parser:
   http_parser:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -644,6 +686,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.17.0"
     version: "0.17.0"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
   js:
   js:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -774,6 +823,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "1.0.0"
     version: "1.0.0"
+  node_preamble:
+    dependency: transitive
+    description:
+      name: node_preamble
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
   octo_image:
   octo_image:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -788,6 +844,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.4.5"
     version: "0.4.5"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
   package_info_plus:
   package_info_plus:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -970,6 +1033,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "3.6.2"
     version: "3.6.2"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.1"
   process:
   process:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -984,6 +1054,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "6.0.3"
     version: "6.0.3"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
   quiver:
   quiver:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1124,11 +1201,53 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "2.1.1"
     version: "2.1.1"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.0"
+  shelf_packages_handler:
+    dependency: transitive
+    description:
+      name: shelf_packages_handler
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  shelf_static:
+    dependency: transitive
+    description:
+      name: shelf_static
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.1"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
   sky_engine:
   sky_engine:
     dependency: transitive
     dependency: transitive
     description: flutter
     description: flutter
     source: sdk
     source: sdk
     version: "0.0.99"
     version: "0.0.99"
+  source_map_stack_trace:
+    dependency: transitive
+    description:
+      name: source_map_stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  source_maps:
+    dependency: transitive
+    description:
+      name: source_maps
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.10.10"
   source_span:
   source_span:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1220,6 +1339,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "1.2.0"
     version: "1.2.0"
+  test:
+    dependency: "direct dev"
+    description:
+      name: test
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.21.1"
   test_api:
   test_api:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1227,6 +1353,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.4.9"
     version: "0.4.9"
+  test_core:
+    dependency: transitive
+    description:
+      name: test_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.13"
   timezone:
   timezone:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1381,6 +1514,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.3.3"
     version: "0.3.3"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.3.0"
   wakelock:
   wakelock:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1423,6 +1563,27 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.0.2"
     version: "0.0.2"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.2"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+  webkit_inspection_protocol:
+    dependency: transitive
+    description:
+      name: webkit_inspection_protocol
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
   win32:
   win32:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 2 - 1
pubspec.yaml

@@ -12,7 +12,7 @@ description: ente photos application
 # Read more about iOS versioning at
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 
 
-version: 0.6.55+385
+version: 0.6.56+386
 
 
 environment:
 environment:
   sdk: '>=2.17.0 <3.0.0'
   sdk: '>=2.17.0 <3.0.0'
@@ -127,6 +127,7 @@ dev_dependencies:
   flutter_lints: ^2.0.1
   flutter_lints: ^2.0.1
   flutter_test:
   flutter_test:
     sdk: flutter
     sdk: flutter
+  test:
 
 
 flutter_icons:
 flutter_icons:
   android: "launcher_icon"
   android: "launcher_icon"

+ 40 - 0
test/utils/date_time_util_test.dart

@@ -0,0 +1,40 @@
+import 'package:flutter/foundation.dart';
+import 'package:photos/core/constants.dart';
+import 'package:photos/utils/date_time_util.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test("parseDateTimeFromFile", () {
+    final List<String> validParsing = [
+      "IMG-20221109-WA0000",
+      '''Screenshot_20220807-195908_Firefox''',
+      '''Screenshot_20220507-195908''',
+      "2019-02-18 16.00.12-DCMX",
+      "20221107_231730",
+      "2020-11-01 02.31.02",
+      "IMG_20210921_144423",
+      "2019-10-31 155703",
+      "IMG_20210921_144423_783",
+      "Screenshot_2022-06-21-16-51-29-164_newFormat",
+    ];
+    for (String val in validParsing) {
+      final parsedValue = parseDateTimeFromFileNameV2(val);
+      expect(
+        parsedValue != null,
+        true,
+        reason: "Failed to parse time from $val",
+      );
+      if (kDebugMode) {
+        debugPrint("Parsed $val as ${parsedValue?.toIso8601String()}");
+      }
+    }
+  });
+
+  test("verify constants", () {
+    expect(
+      jan011981Time,
+      DateTime(1981, 1, 1).toUtc().microsecondsSinceEpoch,
+      reason: "constant mismatch",
+    );
+  });
+}