diff --git a/fastlane/metadata/android/en-US/changelogs/420.txt b/fastlane/metadata/android/en-US/changelogs/420.txt new file mode 100644 index 000000000..b34916cd4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/420.txt @@ -0,0 +1,11 @@ +* Collaborative albums ✨ + +You can now create albums where multiple ente users can add photos! + +Albums can have both collaborators and viewers, and as many as you like. Storage is only counted once, for the person who uploaded the photo + +This will enable many aspects of partner sharing + +* Support for uncategorized photos - keep photos that do not belong to albums. This also allows you to delete albums whilst keeping their contents + +* Redesigned album selector \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 4719ffb07..53cab6c96 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,12 +1,10 @@ ente is a simple app to backup and share your photos and videos. -If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them. +If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them. -We have apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner. +We have open-source apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner. -ente also makes it simple to share your albums with your loved ones, even if they aren't on ente. - -You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app. +ente also makes it simple to share your albums with your loved ones, even if they aren't on ente. You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app. Your encrypted data is replicated to 3 different locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you. @@ -29,7 +27,7 @@ FEATURES - and a LOT more! PERMISSIONS -Ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md PRICING We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io. diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 6678521fc..236419b4d 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -55,3 +55,5 @@ const int intMaxValue = 9223372036854775807; const double restrictedMaxWidth = 430; const double mobileSmallThreshold = 336; + +const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1]; diff --git a/lib/core/network/ente_interceptor.dart b/lib/core/network/ente_interceptor.dart new file mode 100644 index 000000000..5eb8d16d5 --- /dev/null +++ b/lib/core/network/ente_interceptor.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:photos/core/configuration.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uuid/uuid.dart'; + +class EnteRequestInterceptor extends Interceptor { + final SharedPreferences _preferences; + final String enteEndpoint; + + EnteRequestInterceptor(this._preferences, this.enteEndpoint); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (kDebugMode) { + assert( + options.baseUrl == enteEndpoint, + "interceptor should only be used for API endpoint", + ); + } + // ignore: prefer_const_constructors + options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString()); + final String? tokenValue = _preferences.getString(Configuration.tokenKey); + if (tokenValue != null) { + options.headers.putIfAbsent("X-Auth-Token", () => tokenValue); + } + return super.onRequest(options, handler); + } +} diff --git a/lib/core/network.dart b/lib/core/network/network.dart similarity index 59% rename from lib/core/network.dart rename to lib/core/network/network.dart index 0d396e5d7..b3930ddaf 100644 --- a/lib/core/network.dart +++ b/lib/core/network/network.dart @@ -2,16 +2,14 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:fk_user_agent/fk_user_agent.dart'; -import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; +import 'package:photos/core/network/ente_interceptor.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:uuid/uuid.dart'; int kConnectTimeout = 15000; -class Network { +class NetworkClient { // apiEndpoint points to the Ente server's API endpoint static const apiEndpoint = String.fromEnvironment( "endpoint", @@ -46,37 +44,14 @@ class Network { }, ), ); - _enteDio.interceptors.add(EnteRequestInterceptor(preferences)); + _enteDio.interceptors.add(EnteRequestInterceptor(preferences, apiEndpoint)); } - Network._privateConstructor(); + NetworkClient._privateConstructor(); - static Network instance = Network._privateConstructor(); + static NetworkClient instance = NetworkClient._privateConstructor(); Dio getDio() => _dio; Dio get enteDio => _enteDio; } - -class EnteRequestInterceptor extends Interceptor { - final SharedPreferences _preferences; - - EnteRequestInterceptor(this._preferences); - - @override - void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - if (kDebugMode) { - assert( - options.baseUrl == Network.apiEndpoint, - "interceptor should only be used for API endpoint", - ); - } - // ignore: prefer_const_constructors - options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString()); - final String? tokenValue = _preferences.getString(Configuration.tokenKey); - if (tokenValue != null) { - options.headers.putIfAbsent("X-Auth-Token", () => tokenValue); - } - return super.onRequest(options, handler); - } -} diff --git a/lib/db/device_files_db.dart b/lib/db/device_files_db.dart index 79aaa6f46..d967e916d 100644 --- a/lib/db/device_files_db.dart +++ b/lib/db/device_files_db.dart @@ -307,6 +307,7 @@ extension DeviceFiles on FilesDB { Future getFilesInDeviceCollection( DeviceCollection deviceCollection, + int? ownerID, int startTime, int endTime, { int? limit, @@ -319,7 +320,9 @@ extension DeviceFiles on FilesDB { FROM ${FilesDB.filesTable} WHERE ${FilesDB.columnLocalID} IS NOT NULL AND ${FilesDB.columnCreationTime} >= $startTime AND - ${FilesDB.columnCreationTime} <= $endTime AND + ${FilesDB.columnCreationTime} <= $endTime AND + (${FilesDB.columnOwnerID} IS NULL OR ${FilesDB.columnOwnerID} = + $ownerID ) AND ${FilesDB.columnLocalID} IN (SELECT id FROM device_files where path_id = '${deviceCollection.id}' ) ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order diff --git a/lib/db/file_updation_db.dart b/lib/db/file_updation_db.dart index 26e97fb66..554d76150 100644 --- a/lib/db/file_updation_db.dart +++ b/lib/db/file_updation_db.dart @@ -105,7 +105,7 @@ class FileUpdationDB { endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch, ); _logger.info( - "Batch insert of ${fileLocalIDs.length} " + "Batch insert of ${fileLocalIDs.length} updated files due to $reason " "took ${duration.inMilliseconds} ms.", ); } diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 2f436da9c..1b5d1117a 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -763,13 +763,15 @@ class FilesDB { return convertToFiles(results)[0]; } - Future> getExistingLocalFileIDs() async { + Future> getExistingLocalFileIDs(int ownerID) async { final db = await instance.database; final rows = await db.query( filesTable, columns: [columnLocalID], distinct: true, - where: '$columnLocalID IS NOT NULL', + where: '$columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR ' + '$columnOwnerID = ?)', + whereArgs: [ownerID], ); final result = {}; for (final row in rows) { diff --git a/lib/main.dart b/lib/main.dart index 9346e2e42..4548871ab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/core/errors.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/upload_locks_db.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/services/app_lifecycle_service.dart'; @@ -135,7 +135,7 @@ Future _init(bool isBackground, {String via = ''}) async { } CryptoUtil.init(); await NotificationService.instance.init(); - await Network.instance.init(); + await NetworkClient.instance.init(); await Configuration.instance.init(); await UserService.instance.init(); await UserRemoteFlagService.instance.init(); diff --git a/lib/models/collection.dart b/lib/models/collection.dart index e1b6cf1f1..9d1af6e4e 100644 --- a/lib/models/collection.dart +++ b/lib/models/collection.dart @@ -49,6 +49,13 @@ class Collection { return mMdVersion > 0 && magicMetadata.visibility == visibilityArchive; } + // hasLink returns true if there's any link attached to the collection + // including expired links + bool get hasLink => publicURLs != null && publicURLs!.isNotEmpty; + + // hasSharees returns true if the collection is shared with other ente users + bool get hasSharees => sharees != null && sharees!.isNotEmpty; + bool isHidden() { if (isDefaultHidden()) { return true; diff --git a/lib/models/gallery_type.dart b/lib/models/gallery_type.dart index 59adc9c23..d93e42faf 100644 --- a/lib/models/gallery_type.dart +++ b/lib/models/gallery_type.dart @@ -184,4 +184,12 @@ extension GalleyTypeExtension on GalleryType { bool showUnFavoriteOption() { return this == GalleryType.favorite; } + + bool showRestoreOption() { + return this == GalleryType.trash; + } + + bool showPermanentlyDeleteOption() { + return this == GalleryType.trash; + } } diff --git a/lib/services/billing_service.dart b/lib/services/billing_service.dart index 7f685f62e..8f603c894 100644 --- a/lib/services/billing_service.dart +++ b/lib/services/billing_service.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/errors.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; import 'package:photos/models/user_details.dart'; @@ -32,7 +32,7 @@ class BillingService { static final BillingService instance = BillingService._privateConstructor(); final _logger = Logger("BillingService"); - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; bool _isOnSubscriptionPage = false; diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index 7df7af785..f35c29d63 100644 --- a/lib/services/collections_service.dart +++ b/lib/services/collections_service.dart @@ -12,7 +12,7 @@ import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/collections_db.dart'; import 'package:photos/db/device_files_db.dart'; import 'package:photos/db/files_db.dart'; @@ -50,7 +50,7 @@ class CollectionsService { late Configuration _config; late SharedPreferences _prefs; - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; final _localPathToCollectionID = {}; final _collectionIDToCollections = {}; final _cachedKeys = {}; @@ -138,7 +138,7 @@ class CollectionsService { } await _updateDB(updatedCollections); _prefs.setInt(_collectionsSyncTimeKey, maxUpdationTime); - watch.logAndReset("till DB insertion"); + watch.logAndReset("till DB insertion ${updatedCollections.length}"); final collections = await _db.getAllCollections(); for (final collection in collections) { _cacheCollectionAttributes(collection); @@ -159,6 +159,8 @@ class CollectionsService { void clearCache() { _localPathToCollectionID.clear(); _collectionIDToCollections.clear(); + cachedDefaultHiddenCollection = null; + cachedUncategorizedCollection = null; _cachedKeys.clear(); } @@ -238,7 +240,7 @@ class CollectionsService { (u) => u.id == userID, ); if (matchingUser != null) { - _cachedUserIdToUser[userID] = collection.owner!; + _cachedUserIdToUser[userID] = matchingUser; } } } @@ -592,12 +594,16 @@ class CollectionsService { } } - Future createShareUrl(Collection collection) async { + Future createShareUrl( + Collection collection, { + bool enableCollect = false, + }) async { try { final response = await _enteDio.post( "/collections/share-url", data: { "collectionID": collection.id, + "enableCollect": enableCollect, }, ); collection.publicURLs?.add(PublicURL.fromMap(response.data["result"])); @@ -894,8 +900,9 @@ class CollectionsService { final params = {}; params["collectionID"] = toCollectionID; final toCollectionKey = getCollectionKey(toCollectionID); + final int ownerID = Configuration.instance.getUserID()!; final Set existingLocalIDS = - await FilesDB.instance.getExistingLocalFileIDs(); + await FilesDB.instance.getExistingLocalFileIDs(ownerID); final batchedFiles = files.chunks(batchSize); for (final batch in batchedFiles) { params["files"] = []; @@ -1048,7 +1055,7 @@ class CollectionsService { params["fileIDs"].add(file.uploadedFileID); } await _enteDio.post( - "/collections/v2/remove-files", + "/collections/v3/remove-files", data: params, ); diff --git a/lib/services/deduplication_service.dart b/lib/services/deduplication_service.dart index 3ebac05b7..01c534a61 100644 --- a/lib/services/deduplication_service.dart +++ b/lib/services/deduplication_service.dart @@ -1,13 +1,13 @@ import 'package:logging/logging.dart'; import 'package:photos/core/errors.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/models/duplicate_files.dart'; import 'package:photos/models/file.dart'; class DeduplicationService { final _logger = Logger("DeduplicationService"); - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; DeduplicationService._privateConstructor(); diff --git a/lib/services/feature_flag_service.dart b/lib/services/feature_flag_service.dart index 47517d183..2d24dd08b 100644 --- a/lib/services/feature_flag_service.dart +++ b/lib/services/feature_flag_service.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:shared_preferences/shared_preferences.dart'; class FeatureFlagService { @@ -69,7 +69,7 @@ class FeatureFlagService { Future fetchFeatureFlags() async { try { - final response = await Network.instance + final response = await NetworkClient.instance .getDio() .get("https://static.ente.io/feature_flags.json"); final flagsResponse = FeatureFlags.fromMap(response.data); diff --git a/lib/services/file_magic_service.dart b/lib/services/file_magic_service.dart index 4fdc8e056..f3ce08ee6 100644 --- a/lib/services/file_magic_service.dart +++ b/lib/services/file_magic_service.dart @@ -7,7 +7,7 @@ import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/force_reload_home_gallery_event.dart'; @@ -26,7 +26,7 @@ class FileMagicService { FileMagicService._privateConstructor() { _filesDB = FilesDB.instance; - _enteDio = Network.instance.enteDio; + _enteDio = NetworkClient.instance.enteDio; } static final FileMagicService instance = diff --git a/lib/services/files_service.dart b/lib/services/files_service.dart index 2d13f971f..136e3e7c2 100644 --- a/lib/services/files_service.dart +++ b/lib/services/files_service.dart @@ -2,7 +2,7 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:photos/core/configuration.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/extensions/list.dart'; import 'package:photos/models/file.dart'; @@ -17,7 +17,7 @@ class FilesService { late Configuration _config; FilesService._privateConstructor() { - _enteDio = Network.instance.enteDio; + _enteDio = NetworkClient.instance.enteDio; _logger = Logger("FilesService"); _filesDB = FilesDB.instance; _config = Configuration.instance; diff --git a/lib/services/local_sync_service.dart b/lib/services/local_sync_service.dart index 0f3d2c7fd..eb79aeb18 100644 --- a/lib/services/local_sync_service.dart +++ b/lib/services/local_sync_service.dart @@ -33,7 +33,6 @@ class LocalSyncService { static const hasImportedDeviceCollections = "has_imported_device_collections"; static const kHasGrantedPermissionsKey = "has_granted_permissions"; static const kPermissionStateKey = "permission_state"; - static const kEditedFileIDsKey = "edited_file_ids"; // Adding `_2` as a suffic to pull files that were earlier ignored due to permission errors // See https://github.com/CaiJingLong/flutter_photo_manager/issues/589 @@ -75,20 +74,18 @@ class LocalSyncService { return _existingSync!.future; } _existingSync = Completer(); - final existingLocalFileIDs = await _db.getExistingLocalFileIDs(); - _logger.info( - existingLocalFileIDs.length.toString() + " localIDs were discovered", - ); - final editedFileIDs = _getEditedFileIDs().toSet(); + final int ownerID = Configuration.instance.getUserID()!; + final existingLocalFileIDs = await _db.getExistingLocalFileIDs(ownerID); + _logger.info("${existingLocalFileIDs.length} localIDs were discovered"); + final syncStartTime = DateTime.now().microsecondsSinceEpoch; final lastDBUpdationTime = _prefs.getInt(kDbUpdationTimeKey) ?? 0; final startTime = DateTime.now().microsecondsSinceEpoch; if (lastDBUpdationTime != 0) { - await _loadAndStorePhotos( - lastDBUpdationTime, - syncStartTime, + await _loadAndStoreDiff( existingLocalFileIDs, - editedFileIDs, + fromTime: lastDBUpdationTime, + toTime: syncStartTime, ); } else { // Load from 0 - 01.01.2010 @@ -97,25 +94,22 @@ class LocalSyncService { var toYear = 2010; var toTime = DateTime(toYear).microsecondsSinceEpoch; while (toTime < syncStartTime) { - await _loadAndStorePhotos( - startTime, - toTime, + await _loadAndStoreDiff( existingLocalFileIDs, - editedFileIDs, + fromTime: startTime, + toTime: toTime, ); startTime = toTime; toYear++; toTime = DateTime(toYear).microsecondsSinceEpoch; } - await _loadAndStorePhotos( - startTime, - syncStartTime, + await _loadAndStoreDiff( existingLocalFileIDs, - editedFileIDs, + fromTime: startTime, + toTime: syncStartTime, ); } - if (!_prefs.containsKey(kHasCompletedFirstImportKey) || - !(_prefs.getBool(kHasCompletedFirstImportKey)!)) { + if (!hasCompletedFirstImport()) { await _prefs.setBool(kHasCompletedFirstImportKey, true); // mark device collection has imported on first import await _refreshDeviceFolderCountAndCover(isFirstSync: true); @@ -177,7 +171,12 @@ class LocalSyncService { } Future syncAll() async { + if (!Configuration.instance.isLoggedIn()) { + _logger.warning("syncCall called when user is not logged in"); + return false; + } final stopwatch = EnteWatch("localSyncAll")..start(); + final localAssets = await getAllLocalAssets(); _logger.info( "Loading allLocalAssets ${localAssets.length} took ${stopwatch.elapsedMilliseconds}ms ", @@ -186,7 +185,8 @@ class LocalSyncService { _logger.info( "refreshDeviceFolderCountAndCover + allLocalAssets took ${stopwatch.elapsedMilliseconds}ms ", ); - final existingLocalFileIDs = await _db.getExistingLocalFileIDs(); + final int ownerID = Configuration.instance.getUserID()!; + final existingLocalFileIDs = await _db.getExistingLocalFileIDs(ownerID); final Map> pathToLocalIDs = await _db.getDevicePathIDToLocalIDMap(); final invalidIDs = _getInvalidFileIDs().toSet(); @@ -236,15 +236,6 @@ class LocalSyncService { return hasUnsyncedFiles; } - List _getEditedFileIDs() { - if (_prefs.containsKey(kEditedFileIDsKey)) { - return _prefs.getStringList(kEditedFileIDsKey)!; - } else { - final List editedIDs = []; - return editedIDs; - } - } - Future trackInvalidFile(File file) async { if (file.localID == null) { debugPrint("Warning: Invalid file has no localID"); @@ -296,7 +287,6 @@ class LocalSyncService { kHasCompletedFirstImportKey, hasImportedDeviceCollections, kDbUpdationTimeKey, - kEditedFileIDsKey, "has_synced_edit_time", "has_selected_all_folders_for_backup", ]) { @@ -304,39 +294,40 @@ class LocalSyncService { } } - Future _loadAndStorePhotos( - int fromTime, - int toTime, - Set existingLocalFileIDs, - Set editedFileIDs, - ) async { + Future _loadAndStoreDiff( + Set existingLocalDs, { + required int fromTime, + required int toTime, + }) async { final Tuple2, List> result = await getLocalPathAssetsAndFiles(fromTime, toTime, _computer); + + // Update the mapping for device path_id to local file id. Also, keep track + // of newly discovered device paths await FilesDB.instance.insertLocalAssets( result.item1, shouldAutoBackup: Configuration.instance.hasSelectedAllFoldersForBackup(), ); + final List files = result.item2; - _logger.info( - "Loaded ${files.length} photos from " + - DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() + - " to " + - DateTime.fromMicrosecondsSinceEpoch(toTime).toString(), - ); if (files.isNotEmpty) { - await _trackUpdatedFiles( - files, - existingLocalFileIDs, - editedFileIDs, + _logger.info( + "Loaded ${files.length} photos from " + + DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() + + " to " + + DateTime.fromMicrosecondsSinceEpoch(toTime).toString(), ); + await _trackUpdatedFiles(files, existingLocalDs); + // keep reference of all Files for firing LocalPhotosUpdatedEvent final List allFiles = []; allFiles.addAll(files); - files.removeWhere((file) => existingLocalFileIDs.contains(file.localID)); + // remove existing files and insert newly imported files in the table + files.removeWhere((file) => existingLocalDs.contains(file.localID)); await _db.insertMultiple( files, conflictAlgorithm: ConflictAlgorithm.ignore, ); - _logger.info("Inserted " + files.length.toString() + " files."); + _logger.info('Inserted ${files.length} files'); Bus.instance.fire( LocalPhotosUpdatedEvent(allFiles, source: "loadedPhoto"), ); @@ -347,22 +338,16 @@ class LocalSyncService { Future _trackUpdatedFiles( List files, Set existingLocalFileIDs, - Set editedFileIDs, ) async { - final updatedFiles = files - .where((file) => existingLocalFileIDs.contains(file.localID)) + final List updatedLocalIDs = files + .where( + (file) => + file.localID != null && + existingLocalFileIDs.contains(file.localID), + ) + .map((e) => e.localID!) .toList(); - updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID)); - if (updatedFiles.isNotEmpty) { - _logger.info( - updatedFiles.length.toString() + " local files were updated.", - ); - final List updatedLocalIDs = []; - for (final file in updatedFiles) { - if (file.localID != null) { - updatedLocalIDs.add(file.localID!); - } - } + if (updatedLocalIDs.isNotEmpty) { await FileUpdationDB.instance.insertMultiple( updatedLocalIDs, FileUpdationDB.modificationTimeUpdated, diff --git a/lib/services/push_service.dart b/lib/services/push_service.dart index e74f35af9..1bee67a45 100644 --- a/lib/services/push_service.dart +++ b/lib/services/push_service.dart @@ -4,7 +4,7 @@ import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/events/signed_in_event.dart'; import 'package:photos/services/sync_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -74,7 +74,7 @@ class PushService { String fcmToken, String? apnsToken, ) async { - await Network.instance.enteDio.post( + await NetworkClient.instance.enteDio.post( "/push/token", data: { "fcmToken": fcmToken, diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 3e0c03180..6f41bba7a 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -1,6 +1,6 @@ import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/data/holidays.dart'; import 'package:photos/data/months.dart'; import 'package:photos/data/years.dart'; @@ -21,7 +21,7 @@ import 'package:tuple/tuple.dart'; class SearchService { Future>? _cachedFilesFuture; - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; final _logger = Logger((SearchService).toString()); final _collectionService = CollectionsService.instance; static const _maximumResultsLimit = 20; @@ -115,8 +115,8 @@ class SearchService { break; } - if (!c.collection.isHidden() && c.collection.type != CollectionType - .uncategorized && + if (!c.collection.isHidden() && + c.collection.type != CollectionType.uncategorized && c.collection.name!.toLowerCase().contains( query.toLowerCase(), )) { diff --git a/lib/services/sync_service.dart b/lib/services/sync_service.dart index 99bd47856..cdd5e7643 100644 --- a/lib/services/sync_service.dart +++ b/lib/services/sync_service.dart @@ -9,7 +9,7 @@ import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/device_files_db.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/events/permission_granted_event.dart'; @@ -28,7 +28,7 @@ class SyncService { final _logger = Logger("SyncService"); final _localSyncService = LocalSyncService.instance; final _remoteSyncService = RemoteSyncService.instance; - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; final _uploader = FileUploader.instance; bool _syncStopRequested = false; Completer? _existingSync; diff --git a/lib/services/trash_sync_service.dart b/lib/services/trash_sync_service.dart index 961436ef2..4178331bd 100644 --- a/lib/services/trash_sync_service.dart +++ b/lib/services/trash_sync_service.dart @@ -4,7 +4,7 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/trash_db.dart'; import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/force_reload_trash_page_event.dart'; @@ -29,7 +29,7 @@ class TrashSyncService { static final TrashSyncService instance = TrashSyncService._privateConstructor(); - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; void init(SharedPreferences preferences) { _prefs = preferences; diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index 570449214..ee051ae68 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:photos/core/constants.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/services/notification_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; @@ -98,7 +98,7 @@ class UpdateService { } Future _getLatestVersionInfo() async { - final response = await Network.instance + final response = await NetworkClient.instance .getDio() .get("https://ente.io/release-info/independent.json"); return LatestVersionInfo.fromMap(response.data["latestVersion"]); diff --git a/lib/services/user_remote_flag_service.dart b/lib/services/user_remote_flag_service.dart index b850deae8..3a1d122e0 100644 --- a/lib/services/user_remote_flag_service.dart +++ b/lib/services/user_remote_flag_service.dart @@ -4,13 +4,13 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/events/notification_event.dart'; import 'package:photos/services/user_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class UserRemoteFlagService { - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; final _logger = Logger((UserRemoteFlagService).toString()); late SharedPreferences _prefs; diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index e4bf4c13b..a9dc5c013 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -9,7 +9,7 @@ import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/public_keys_db.dart'; import 'package:photos/events/two_factor_status_change_event.dart'; import 'package:photos/events/user_details_changed_event.dart'; @@ -37,8 +37,8 @@ import 'package:shared_preferences/shared_preferences.dart'; class UserService { static const keyHasEnabledTwoFactor = "has_enabled_two_factor"; static const keyUserDetails = "user_details"; - final _dio = Network.instance.getDio(); - final _enteDio = Network.instance.enteDio; + final _dio = NetworkClient.instance.getDio(); + final _enteDio = NetworkClient.instance.enteDio; final _logger = Logger((UserService).toString()); final _config = Configuration.instance; late SharedPreferences _preferences; @@ -117,6 +117,8 @@ class UserService { } } + // getPublicKey returns null value if email id is not + // associated with another ente account Future getPublicKey(String email) async { try { final response = await _enteDio.get( @@ -127,8 +129,10 @@ class UserService { await PublicKeysDB.instance.setKey(PublicKey(email, publicKey)); return publicKey; } on DioError catch (e) { - _logger.info(e); - return null; + if (e.response != null && e.response?.statusCode == 404) { + return null; + } + rethrow; } } @@ -197,21 +201,23 @@ class UserService { } Future logout(BuildContext context) async { - final dialog = createProgressDialog(context, "Logging out..."); - await dialog.show(); try { final response = await _enteDio.post("/users/logout"); if (response.statusCode == 200) { await Configuration.instance.logout(); - await dialog.hide(); Navigator.of(context).popUntil((route) => route.isFirst); } else { throw Exception("Log out action failed"); } } catch (e) { _logger.severe(e); - await dialog.hide(); - showGenericErrorDialog(context: context); + //This future is for waiting for the dialog from which logout() is called + //to close and only then to show the error dialog. + Future.delayed( + const Duration(milliseconds: 150), + () => showGenericErrorDialog(context: context), + ); + rethrow; } } diff --git a/lib/ui/account/delete_account_page.dart b/lib/ui/account/delete_account_page.dart index c6cc97ba0..a0e4bccc1 100644 --- a/lib/ui/account/delete_account_page.dart +++ b/lib/ui/account/delete_account_page.dart @@ -131,7 +131,7 @@ class DeleteAccountPage extends StatelessWidget { ); if (hasAuthenticated) { - final choice = await showNewChoiceDialog( + final choice = await showChoiceDialog( context, title: 'Are you sure you want to delete your account?', body: diff --git a/lib/ui/account/password_reentry_page.dart b/lib/ui/account/password_reentry_page.dart index 30650f07d..c8b0601f5 100644 --- a/lib/ui/account/password_reentry_page.dart +++ b/lib/ui/account/password_reentry_page.dart @@ -80,7 +80,7 @@ class _PasswordReentryPageState extends State { } on KeyDerivationError catch (e, s) { _logger.severe("Password verification failed", e, s); await dialog.hide(); - final dialogChoice = await showNewChoiceDialog( + final dialogChoice = await showChoiceDialog( context, title: "Recreate password", body: "The current device is not powerful enough to verify your " @@ -102,7 +102,7 @@ class _PasswordReentryPageState extends State { } catch (e, s) { _logger.severe("Password verification failed", e, s); await dialog.hide(); - final dialogChoice = await showNewChoiceDialog( + final dialogChoice = await showChoiceDialog( context, title: "Incorrect password", body: "Please try again", diff --git a/lib/ui/account/sessions_page.dart b/lib/ui/account/sessions_page.dart index 80fe7cc0a..be219e582 100644 --- a/lib/ui/account/sessions_page.dart +++ b/lib/ui/account/sessions_page.dart @@ -22,7 +22,9 @@ class _SessionsPageState extends State { @override void initState() { - _fetchActiveSessions(); + _fetchActiveSessions().onError((error, stackTrace) { + showToast(context, "Failed to fetch active sessions"); + }); super.initState(); } @@ -115,9 +117,9 @@ class _SessionsPageState extends State { await UserService.instance.terminateSession(session.token); await _fetchActiveSessions(); await dialog.hide(); - } catch (e, s) { + } catch (e) { await dialog.hide(); - _logger.severe('failed to terminate', e, s); + _logger.severe('failed to terminate'); showErrorDialog( context, 'Oops', @@ -127,17 +129,17 @@ class _SessionsPageState extends State { } Future _fetchActiveSessions() async { - _sessions = await UserService.instance - .getActiveSessions() - .onError((error, stackTrace) { - showToast(context, "Failed to fetch active sessions"); - throw error!; + _sessions = await UserService.instance.getActiveSessions().onError((e, s) { + _logger.severe("failed to fetch active sessions", e, s); + throw e!; }); if (_sessions != null) { _sessions!.sessions.sort((first, second) { return second.lastUsedTime.compareTo(first.lastUsedTime); }); - setState(() {}); + if (mounted) { + setState(() {}); + } } } diff --git a/lib/ui/account/verify_recovery_page.dart b/lib/ui/account/verify_recovery_page.dart index 3b8680a0a..4c7b42ef1 100644 --- a/lib/ui/account/verify_recovery_page.dart +++ b/lib/ui/account/verify_recovery_page.dart @@ -74,7 +74,7 @@ class _VerifyRecoveryPageState extends State { "The recovery key you entered is not valid. Please make sure it " "contains 24 words, and check the spelling of each.\n\nIf you " "entered an older recovery code, make sure it is 64 characters long, and check each of them."; - final result = await showNewChoiceDialog( + final result = await showChoiceDialog( context, title: "Invalid key", body: errMessage, diff --git a/lib/ui/actions/collection/collection_file_actions.dart b/lib/ui/actions/collection/collection_file_actions.dart index 57572e22c..846f56744 100644 --- a/lib/ui/actions/collection/collection_file_actions.dart +++ b/lib/ui/actions/collection/collection_file_actions.dart @@ -1,10 +1,8 @@ import 'package:flutter/cupertino.dart'; -import 'package:photos/db/files_db.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/models/file.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/favorites_service.dart'; -import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; @@ -18,13 +16,15 @@ extension CollectionFileActions on CollectionActions { BuildContext bContext, Collection collection, SelectedFiles selectedFiles, + bool removingOthersFile, ) async { final actionResult = await showActionSheet( context: bContext, buttons: [ ButtonWidget( - labelText: "Yes, remove", - buttonType: ButtonType.neutral, + labelText: "Remove", + buttonType: + removingOthersFile ? ButtonType.critical : ButtonType.neutral, buttonSize: ButtonSize.large, shouldStickToDarkTheme: true, isInAlert: true, @@ -50,9 +50,11 @@ extension CollectionFileActions on CollectionActions { isInAlert: true, ), ], - title: "Remove from album?", - body: "Selected items will be removed from this album. Items which are " - "only in this album will be moved to Uncategorized.", + title: removingOthersFile ? "Remove from album?" : null, + body: removingOthersFile + ? "Some of the items you are removing were " + "added by other people, and you will lose access to them" + : "Selected items will be removed from this album", actionSheetType: ActionSheetType.defaultActionSheet, ); if (actionResult != null && actionResult == ButtonAction.error) { @@ -62,103 +64,6 @@ extension CollectionFileActions on CollectionActions { } } - Future showRemoveFromCollectionSheet( - BuildContext context, - Collection collection, - SelectedFiles selectedFiles, - ) async { - final count = selectedFiles.files.length; - final textTheme = getEnteTextTheme(context); - final showDeletePrompt = await _anyItemPresentOnlyInCurrentAlbum( - selectedFiles.files, - collection.id, - ); - final String title = - showDeletePrompt ? "Delete items?" : "Remove from album?"; - final String message1 = showDeletePrompt - ? "Some of the selected items are present only in this album and will be deleted." - : "Selected items will be removed from this album."; - - final String message2 = showDeletePrompt - ? "\n\nItems which are also " - "present in other albums will be removed from this album but will remain elsewhere." - : ""; - - final action = CupertinoActionSheet( - title: Text( - title, - style: textTheme.h3Bold, - textAlign: TextAlign.left, - ), - message: RichText( - text: TextSpan( - children: [ - TextSpan(text: message1, style: textTheme.body), - TextSpan(text: message2, style: textTheme.body) - ], - ), - ), - actions: [ - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - final dialog = createProgressDialog( - context, - showDeletePrompt ? "Deleting files..." : "Removing files...", - ); - await dialog.show(); - try { - await collectionsService.removeFromCollection( - collection.id, - selectedFiles.files.toList(), - ); - await dialog.hide(); - selectedFiles.clearAll(); - } catch (e, s) { - logger.severe(e, s); - await dialog.hide(); - showGenericErrorDialog(context: context); - } - }, - child: Text(showDeletePrompt ? "Yes, delete" : "Yes, remove"), - ), - ], - cancelButton: CupertinoActionSheetAction( - child: const Text("Cancel"), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }, - ), - ); - await showCupertinoModalPopup(context: context, builder: (_) => action); - } - - // check if any of the file only belongs in the given collection id. - // if true, then we need to warn the user that some of the items will be - // deleted - Future _anyItemPresentOnlyInCurrentAlbum( - Set files, - int collectionID, - ) async { - final List uploadedIDs = files - .where((e) => e.uploadedFileID != null) - .map((e) => e.uploadedFileID!) - .toList(); - - final Map> collectionToFilesMap = - await FilesDB.instance.getAllFilesGroupByCollectionID(uploadedIDs); - final Set ids = uploadedIDs.toSet(); - for (MapEntry> entry in collectionToFilesMap.entries) { - if (entry.key == collectionID) { - logger.finest('ignore the collection from which remove is happening'); - continue; - } - ids.removeAll(entry.value.map((f) => f.uploadedFileID!).toSet()); - } - return ids.isNotEmpty; - } - Future updateFavorites( BuildContext context, List files, diff --git a/lib/ui/actions/collection/collection_sharing_actions.dart b/lib/ui/actions/collection/collection_sharing_actions.dart index 352ee0fd9..b2a92437d 100644 --- a/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/lib/ui/actions/collection/collection_sharing_actions.dart @@ -13,8 +13,10 @@ import 'package:photos/services/hidden_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/payment/subscription.dart'; import 'package:photos/utils/date_time_util.dart'; @@ -29,59 +31,18 @@ class CollectionActions { CollectionActions(this.collectionsService); - Future publicLinkToggle( + Future enableUrl( BuildContext context, - Collection collection, - bool enable, - ) async { - // confirm if user wants to disable the url - if (!enable) { - final ButtonAction? result = await showActionSheet( - context: context, - buttons: [ - ButtonWidget( - buttonType: ButtonType.critical, - isInAlert: true, - shouldStickToDarkTheme: true, - buttonAction: ButtonAction.first, - shouldSurfaceExecutionStates: true, - labelText: "Yes, remove", - onTap: () async { - await CollectionsService.instance.disableShareUrl(collection); - }, - ), - const ButtonWidget( - buttonType: ButtonType.secondary, - buttonAction: ButtonAction.cancel, - isInAlert: true, - shouldStickToDarkTheme: true, - labelText: "Cancel", - ) - ], - title: "Remove public link", - body: - 'This will remove the public link for accessing "${collection.name}".', - ); - if (result != null) { - if (result == ButtonAction.error) { - showGenericErrorDialog(context: context); - } - return result == ButtonAction.first; - } else { - return false; - } - } - final dialog = createProgressDialog( - context, - "Creating link...", - ); + Collection collection, { + bool enableCollect = false, + }) async { try { - await dialog.show(); - await CollectionsService.instance.createShareUrl(collection); - dialog.hide(); + await CollectionsService.instance.createShareUrl( + collection, + enableCollect: enableCollect, + ); return true; } catch (e) { - dialog.hide(); if (e is SharingNotPermittedForFreeAccountsError) { _showUnSupportedAlert(context); } else { @@ -92,6 +53,43 @@ class CollectionActions { } } + Future disableUrl(BuildContext context, Collection collection) async { + final ButtonAction? result = await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + buttonType: ButtonType.critical, + isInAlert: true, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: true, + labelText: "Yes, remove", + onTap: () async { + await CollectionsService.instance.disableShareUrl(collection); + }, + ), + const ButtonWidget( + buttonType: ButtonType.secondary, + buttonAction: ButtonAction.cancel, + isInAlert: true, + shouldStickToDarkTheme: true, + labelText: "Cancel", + ) + ], + title: "Remove public link", + body: + 'This will remove the public link for accessing "${collection.name}".', + ); + if (result != null) { + if (result == ButtonAction.error) { + showGenericErrorDialog(context: context); + } + return result == ButtonAction.first; + } else { + return false; + } + } + Future createSharedCollectionLink( BuildContext context, List files, @@ -137,44 +135,55 @@ class CollectionActions { } // removeParticipant remove the user from a share album - Future removeParticipant( + Future removeParticipant( BuildContext context, Collection collection, User user, ) async { - final result = await showNewChoiceDialog( - context, - title: "Remove", - body: "${user.email} will be removed", - firstButtonLabel: "Yes, remove", - firstButtonOnTap: () async { - try { - final newSharees = await CollectionsService.instance - .unshare(collection.id, user.email); - collection.updateSharees(newSharees); - } catch (e, s) { - Logger("EmailItemWidget").severe(e, s); - rethrow; - } - }, + final ButtonAction? result = await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + buttonType: ButtonType.critical, + isInAlert: true, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: true, + labelText: "Yes, remove", + onTap: () async { + final newSharees = await CollectionsService.instance + .unshare(collection.id, user.email); + collection.updateSharees(newSharees); + }, + ), + const ButtonWidget( + buttonType: ButtonType.secondary, + buttonAction: ButtonAction.cancel, + isInAlert: true, + shouldStickToDarkTheme: true, + labelText: "Cancel", + ) + ], + title: "Remove?", + body: '${user.email} will be removed from this shared album\n\nAny ' + 'photos added by them will also be removed from the album', ); - if (result == ButtonAction.error) { - await showGenericErrorDialog(context: context); - return false; - } - if (result == ButtonAction.first) { - return true; - } else { - return false; + if (result != null) { + if (result == ButtonAction.error) { + showGenericErrorDialog(context: context); + } + return result == ButtonAction.first; } + return false; } - Future addEmailToCollection( + // addEmailToCollection returns true if add operation was successful + Future addEmailToCollection( BuildContext context, Collection collection, - String email, { - CollectionParticipantRole role = CollectionParticipantRole.viewer, - String? publicKey, + String email, + CollectionParticipantRole role, { + bool showProgress = false, }) async { if (!isValidEmail(email)) { await showErrorDialog( @@ -182,80 +191,64 @@ class CollectionActions { "Invalid email address", "Please enter a valid email address.", ); - return null; - } else if (email == Configuration.instance.getEmail()) { + return false; + } else if (email.trim() == Configuration.instance.getEmail()) { await showErrorDialog(context, "Oops", "You cannot share with yourself"); - return null; - } else { - // if (collection.getSharees().any((user) => user.email == email)) { - // showErrorDialog( - // context, - // "Oops", - // "You're already sharing this with " + email, - // ); - // return null; - // } + return false; } - if (publicKey == null) { - final dialog = createProgressDialog(context, "Searching for user..."); + + ProgressDialog? dialog; + String? publicKey; + if (showProgress) { + dialog = createProgressDialog(context, "Sharing...", isDismissible: true); await dialog.show(); - try { - publicKey = await UserService.instance.getPublicKey(email); - await dialog.hide(); - } catch (e) { - logger.severe("Failed to get public key", e); - showGenericErrorDialog(context: context); - await dialog.hide(); - } } - // getPublicKey can return null - // ignore: unnecessary_null_comparison + + try { + publicKey = await UserService.instance.getPublicKey(email); + } catch (e) { + await dialog?.hide(); + logger.severe("Failed to get public key", e); + showGenericErrorDialog(context: context); + return false; + } + // getPublicKey can return null when no user is associated with given + // email id if (publicKey == null || publicKey == '') { - final dialog = AlertDialog( - title: const Text("Invite to ente?"), - content: Text( - "Looks like " + - email + - " hasn't signed up for ente yet. would you like to invite them?", - style: const TextStyle( - height: 1.4, - ), - ), - actions: [ - TextButton( - child: Text( - "Invite", - style: TextStyle( - color: Theme.of(context).colorScheme.greenAlternative, - ), - ), - onPressed: () { + // todo: neeraj replace this as per the design where a new screen + // is used for error. Do this change along with handling of network errors + await showDialogWidget( + context: context, + title: "Invite to ente", + icon: Icons.info_outline, + body: "$email does not have an ente account\n\nSend them an invite to" + " add them after they sign up", + isDismissible: true, + buttons: [ + ButtonWidget( + buttonType: ButtonType.neutral, + icon: Icons.adaptive.share, + labelText: "Send invite", + isInAlert: true, + onTap: () async { shareText( - "Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.", + "Download ente so we can easily share original quality photos" + " and videos\n\nhttps://ente.io/#download", ); }, ), ], ); - await showDialog( - context: context, - builder: (BuildContext context) { - return dialog; - }, - ); - return null; + return false; } else { - final dialog = createProgressDialog(context, "Sharing..."); - await dialog.show(); try { final newSharees = await CollectionsService.instance .share(collection.id, email, publicKey, role); + await dialog?.hide(); collection.updateSharees(newSharees); - await dialog.hide(); - showShortToast(context, "Shared successfully!"); return true; } catch (e) { - await dialog.hide(); + await dialog?.hide(); if (e is SharingNotPermittedForFreeAccountsError) { _showUnSupportedAlert(context); } else { @@ -267,6 +260,7 @@ class CollectionActions { } } + // deleteCollectionSheet returns true if the album is successfully deleted Future deleteCollectionSheet( BuildContext bContext, Collection collection, @@ -276,6 +270,13 @@ class CollectionActions { if (collection.owner!.id != currentUserID) { throw AssertionError("Can not delete album owned by others"); } + if (collection.hasSharees) { + final bool confirmDelete = + await _confirmSharedAlbumDeletion(bContext, collection); + if (!confirmDelete) { + return false; + } + } final actionResult = await showActionSheet( context: bContext, buttons: [ @@ -293,8 +294,9 @@ class CollectionActions { await moveFilesFromCurrentCollection(bContext, collection, files); // collection should be empty on server now await collectionsService.trashEmptyCollection(collection); - } catch (e) { - logger.severe("Failed to keep photos and delete collection", e); + } catch (e, s) { + logger.severe( + "Failed to keep photos and delete collection", e, s); rethrow; } }, @@ -356,6 +358,23 @@ class CollectionActions { return false; } + // _confirmSharedAlbumDeletion should be shown when user tries to delete an + // album shared with other ente users. + Future _confirmSharedAlbumDeletion( + BuildContext context, + Collection collection, + ) async { + final ButtonAction? result = await showChoiceActionSheet( + context, + isCritical: true, + title: "Delete shared album?", + firstButtonLabel: "Delete album", + body: "The album will be deleted for everyone\n\nYou will lose access to " + "shared photos in this album that are owned by others", + ); + return result != null && result == ButtonAction.first; + } + /* _moveFilesFromCurrentCollection removes the file from the current collection. Based on the file and collection ownership, files will be @@ -380,19 +399,26 @@ class CollectionActions { ) async { final int currentUserID = Configuration.instance.getUserID()!; final isCollectionOwner = collection.owner!.id == currentUserID; - if (!isCollectionOwner) { - // Todo: Support for removing own files from a collection owner by - // someone else will be added along with collaboration changes - showShortToast(context, "Only collection owner can remove"); - return; - } final FilesSplit split = FilesSplit.split( files, Configuration.instance.getUserID()!, ); - if (split.ownedByOtherUsers.isNotEmpty) { - // Todo: Support for removing own files from a collection owner by - // someone else will be added along with collaboration changes + if (isCollectionOwner && split.ownedByOtherUsers.isNotEmpty) { + await collectionsService.removeFromCollection( + collection.id, + split.ownedByOtherUsers, + ); + } else if (!isCollectionOwner && split.ownedByCurrentUser.isNotEmpty) { + // collection is not owned by the user, just remove files owned + // by current user and return + await collectionsService.removeFromCollection( + collection.id, + split.ownedByCurrentUser, + ); + return; + } + + if (!isCollectionOwner && split.ownedByOtherUsers.isNotEmpty) { showShortToast(context, "Can only remove files owned by you"); return; } diff --git a/lib/ui/advanced_settings_screen.dart b/lib/ui/advanced_settings_screen.dart index 0e7817dbf..35cf72604 100644 --- a/lib/ui/advanced_settings_screen.dart +++ b/lib/ui/advanced_settings_screen.dart @@ -1,15 +1,12 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:photos/core/constants.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/events/force_reload_home_gallery_event.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/icon_button_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; import 'package:photos/ui/tools/debug/app_storage_viewer.dart'; +import 'package:photos/ui/viewer/gallery/photo_grid_size_picker_page.dart'; import 'package:photos/utils/local_settings.dart'; import 'package:photos/utils/navigation_util.dart'; @@ -21,12 +18,11 @@ class AdvancedSettingsScreen extends StatefulWidget { } class _AdvancedSettingsScreenState extends State { - late int _photoGridSize, _chosenGridSize; + late int _photoGridSize; @override void initState() { _photoGridSize = LocalSettings.instance.getPhotoGridSize(); - _chosenGridSize = _photoGridSize; super.initState(); } @@ -66,7 +62,15 @@ class _AdvancedSettingsScreenState extends State { children: [ GestureDetector( onTap: () { - _showPhotoGridSizePicker(delegateBuildContext); + routeToPage( + context, + const PhotoGridSizePickerPage(), + ).then((value) { + setState(() { + _photoGridSize = LocalSettings.instance + .getPhotoGridSize(); + }); + }); }, child: MenuItemWidget( captionedTextWidget: CaptionedTextWidget( @@ -78,9 +82,8 @@ class _AdvancedSettingsScreenState extends State { Icons.chevron_right_outlined, color: colorScheme.strokeBase, ), - borderRadius: 8, + singleBorderRadius: 8, alignCaptionedTextToLeft: true, - // isBottomBorderRadiusRemoved: true, isGestureDetectorDisabled: true, ), ), @@ -96,9 +99,9 @@ class _AdvancedSettingsScreenState extends State { Icons.chevron_right_outlined, color: colorScheme.strokeBase, ), - borderRadius: 8, + singleBorderRadius: 8, alignCaptionedTextToLeft: true, - onTap: () { + onTap: () async { routeToPage(context, const AppStorageViewer()); }, ), @@ -116,106 +119,4 @@ class _AdvancedSettingsScreenState extends State { ), ); } - - Future _showPhotoGridSizePicker(BuildContext buildContext) async { - final textTheme = getEnteTextTheme(buildContext); - final List options = []; - for (int gridSize = photoGridSizeMin; - gridSize <= photoGridSizeMax; - gridSize++) { - options.add( - Text( - gridSize.toString(), - style: textTheme.body, - ), - ); - } - return showCupertinoModalPopup( - context: context, - builder: (context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - decoration: BoxDecoration( - color: getEnteColorScheme(buildContext).backgroundElevated2, - border: const Border( - bottom: BorderSide( - color: Color(0xff999999), - width: 0.0, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CupertinoButton( - onPressed: () { - Navigator.of(context).pop('cancel'); - }, - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 5.0, - ), - child: Text( - 'Cancel', - style: textTheme.body, - ), - ), - CupertinoButton( - onPressed: () async { - await LocalSettings.instance - .setPhotoGridSize(_chosenGridSize); - Bus.instance.fire( - ForceReloadHomeGalleryEvent("grid size changed"), - ); - _photoGridSize = _chosenGridSize; - setState(() {}); - Navigator.of(context).pop(''); - }, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 2.0, - ), - child: Text( - 'Confirm', - style: textTheme.body, - ), - ) - ], - ), - ), - Container( - height: 220.0, - color: const Color(0xfff7f7f7), - child: CupertinoPicker( - backgroundColor: - getEnteColorScheme(buildContext).backgroundElevated, - onSelectedItemChanged: (index) { - _chosenGridSize = _getPhotoGridSizeFromIndex(index); - setState(() {}); - }, - scrollController: FixedExtentScrollController( - initialItem: _getIndexFromPhotoGridSize(_chosenGridSize), - ), - magnification: 1.3, - useMagnifier: true, - itemExtent: 25, - diameterRatio: 1, - children: options, - ), - ) - ], - ); - }, - ); - } - - int _getPhotoGridSizeFromIndex(int index) { - return index + 2; - } - - int _getIndexFromPhotoGridSize(int gridSize) { - return gridSize - 2; - } } diff --git a/lib/ui/backup_settings_screen.dart b/lib/ui/backup_settings_screen.dart index 8b838bb81..a908b21d9 100644 --- a/lib/ui/backup_settings_screen.dart +++ b/lib/ui/backup_settings_screen.dart @@ -6,7 +6,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/icon_button_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; @@ -65,7 +65,7 @@ class BackupSettingsScreen extends StatelessWidget { ); }, ), - borderRadius: 8, + singleBorderRadius: 8, alignCaptionedTextToLeft: true, isBottomBorderRadiusRemoved: true, isGestureDetectorDisabled: true, @@ -87,7 +87,7 @@ class BackupSettingsScreen extends StatelessWidget { !Configuration.instance.shouldBackupVideos(), ), ), - borderRadius: 8, + singleBorderRadius: 8, alignCaptionedTextToLeft: true, isTopBorderRadiusRemoved: true, isGestureDetectorDisabled: true, @@ -115,7 +115,7 @@ class BackupSettingsScreen extends StatelessWidget { ); }, ), - borderRadius: 8, + singleBorderRadius: 8, alignCaptionedTextToLeft: true, isGestureDetectorDisabled: true, ), diff --git a/lib/ui/common/dialogs.dart b/lib/ui/common/dialogs.dart deleted file mode 100644 index 3b1c2cb40..000000000 --- a/lib/ui/common/dialogs.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; - -enum DialogUserChoice { firstChoice, secondChoice } - -enum ActionType { - confirm, - critical, -} - -// if dialog is dismissed by tapping outside, this will return null -Future showChoiceDialog( - BuildContext context, - String title, - String content, { - String firstAction = 'Ok', - Color? firstActionColor, - String secondAction = 'Cancel', - Color? secondActionColor, - ActionType actionType = ActionType.confirm, -}) { - final AlertDialog alert = AlertDialog( - title: Text( - title, - style: TextStyle( - color: actionType == ActionType.critical - ? Colors.red - : Theme.of(context).colorScheme.primary, - ), - ), - content: Text( - content, - style: const TextStyle( - height: 1.4, - ), - ), - actions: [ - TextButton( - child: Text( - firstAction, - style: TextStyle( - color: firstActionColor ?? - (actionType == ActionType.critical - ? Colors.red - : Theme.of(context).colorScheme.onSurface), - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop(DialogUserChoice.firstChoice); - }, - ), - TextButton( - child: Text( - secondAction, - style: TextStyle( - color: secondActionColor ?? - Theme.of(context).colorScheme.greenAlternative, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop(DialogUserChoice.secondChoice); - }, - ), - ], - ); - - return showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - barrierColor: Colors.black87, - ); -} diff --git a/lib/ui/components/action_sheet_widget.dart b/lib/ui/components/action_sheet_widget.dart index 305905a33..b20aea7bb 100644 --- a/lib/ui/components/action_sheet_widget.dart +++ b/lib/ui/components/action_sheet_widget.dart @@ -105,7 +105,7 @@ class ActionSheetWidget extends StatelessWidget { isTitleAndBodyNull ? const SizedBox.shrink() : Padding( - padding: const EdgeInsets.only(bottom: 28), + padding: const EdgeInsets.only(bottom: 36), child: ContentContainerWidget( title: title, bodyWidget: bodyWidget, diff --git a/lib/ui/components/album_list_item_widget.dart b/lib/ui/components/album_list_item_widget.dart index da8352692..ba99a0883 100644 --- a/lib/ui/components/album_list_item_widget.dart +++ b/lib/ui/components/album_list_item_widget.dart @@ -37,6 +37,7 @@ class AlbumListItemWidget extends StatelessWidget { ? ThumbnailWidget( item.thumbnail, showFavForAlbumOnly: true, + shouldShowOwnerAvatar: false, ) : const NoThumbnailWidget( addBorder: false, diff --git a/lib/ui/components/dialog_widget.dart b/lib/ui/components/dialog_widget.dart index 0999c3664..df26b12da 100644 --- a/lib/ui/components/dialog_widget.dart +++ b/lib/ui/components/dialog_widget.dart @@ -112,7 +112,7 @@ class ContentContainer extends StatelessWidget { children: [ Icon( icon, - size: 48, + size: 32, ), ], ), diff --git a/lib/ui/components/expandable_menu_item_widget.dart b/lib/ui/components/expandable_menu_item_widget.dart index fdd6e0dbd..6bc959063 100644 --- a/lib/ui/components/expandable_menu_item_widget.dart +++ b/lib/ui/components/expandable_menu_item_widget.dart @@ -2,7 +2,7 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/ui/settings/inherited_settings_state.dart'; diff --git a/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart b/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart new file mode 100644 index 000000000..59f7f69a5 --- /dev/null +++ b/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/common/loading_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; + +class TrailingWidget extends StatefulWidget { + final ValueNotifier executionStateNotifier; + final IconData? trailingIcon; + final Color? trailingIconColor; + final Widget? trailingWidget; + final bool trailingIconIsMuted; + final double trailingExtraMargin; + final bool showExecutionStates; + const TrailingWidget({ + required this.executionStateNotifier, + this.trailingIcon, + this.trailingIconColor, + this.trailingWidget, + required this.trailingIconIsMuted, + required this.trailingExtraMargin, + required this.showExecutionStates, + super.key, + }); + @override + State createState() => _TrailingWidgetState(); +} + +class _TrailingWidgetState extends State { + Widget? trailingWidget; + @override + void initState() { + widget.showExecutionStates + ? widget.executionStateNotifier.addListener(_executionStateListener) + : null; + super.initState(); + } + + @override + void dispose() { + widget.executionStateNotifier.removeListener(_executionStateListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (trailingWidget == null || !widget.showExecutionStates) { + _setTrailingIcon(); + } + return AnimatedSwitcher( + duration: const Duration(milliseconds: 175), + switchInCurve: Curves.easeInExpo, + switchOutCurve: Curves.easeOutExpo, + child: trailingWidget, + ); + } + + void _executionStateListener() { + final colorScheme = getEnteColorScheme(context); + setState(() { + if (widget.executionStateNotifier.value == ExecutionState.idle) { + _setTrailingIcon(); + } else if (widget.executionStateNotifier.value == + ExecutionState.inProgress) { + trailingWidget = EnteLoadingWidget( + color: colorScheme.strokeMuted, + ); + } else if (widget.executionStateNotifier.value == + ExecutionState.successful) { + trailingWidget = Icon( + Icons.check_outlined, + size: 22, + color: colorScheme.primary500, + ); + } else { + trailingWidget = const SizedBox.shrink(); + } + }); + } + + void _setTrailingIcon() { + if (widget.trailingIcon != null) { + trailingWidget = Padding( + padding: EdgeInsets.only( + right: widget.trailingExtraMargin, + ), + child: Icon( + widget.trailingIcon, + color: widget.trailingIconIsMuted + ? getEnteColorScheme(context).strokeMuted + : widget.trailingIconColor, + ), + ); + } else { + trailingWidget = widget.trailingWidget ?? const SizedBox.shrink(); + } + } +} + +class ExpansionTrailingIcon extends StatelessWidget { + final bool isExpanded; + final IconData? trailingIcon; + final Color? trailingIconColor; + const ExpansionTrailingIcon({ + required this.isExpanded, + this.trailingIcon, + this.trailingIconColor, + super.key, + }); + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + opacity: isExpanded ? 0 : 1, + child: AnimatedSwitcher( + transitionBuilder: (child, animation) { + return ScaleTransition(scale: animation, child: child); + }, + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + child: isExpanded + ? const SizedBox.shrink() + : Icon( + trailingIcon, + color: trailingIconColor, + ), + ), + ); + } +} + +class LeadingWidget extends StatelessWidget { + final IconData? leadingIcon; + final Color? leadingIconColor; + + final Widget? leadingIconWidget; + // leadIconSize deafult value is 20. + final double leadingIconSize; + const LeadingWidget({ + required this.leadingIconSize, + this.leadingIcon, + this.leadingIconColor, + this.leadingIconWidget, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + height: leadingIconSize, + width: leadingIconSize, + child: leadingIcon == null + ? (leadingIconWidget != null + ? FittedBox( + fit: BoxFit.contain, + child: leadingIconWidget, + ) + : const SizedBox.shrink()) + : FittedBox( + fit: BoxFit.contain, + child: Icon( + leadingIcon, + color: leadingIconColor ?? + getEnteColorScheme(context).strokeBase, + ), + ), + ), + ); + } +} diff --git a/lib/ui/components/menu_item_widget.dart b/lib/ui/components/menu_item_widget/menu_item_widget.dart similarity index 50% rename from lib/ui/components/menu_item_widget.dart rename to lib/ui/components/menu_item_widget/menu_item_widget.dart index c2f6ad85c..f89d60a85 100644 --- a/lib/ui/components/menu_item_widget.dart +++ b/lib/ui/components/menu_item_widget/menu_item_widget.dart @@ -1,7 +1,17 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart'; +import 'package:photos/utils/debouncer.dart'; + +enum ExecutionState { + idle, + inProgress, + error, + successful; +} + +typedef FutureVoidCallback = Future Function(); class MenuItemWidget extends StatefulWidget { final Widget captionedTextWidget; @@ -13,6 +23,7 @@ class MenuItemWidget extends StatefulWidget { final Color? leadingIconColor; final Widget? leadingIconWidget; + // leadIconSize deafult value is 20. final double leadingIconSize; @@ -25,11 +36,17 @@ class MenuItemWidget extends StatefulWidget { /// If provided, add this much extra spacing to the right of the trailing icon. final double trailingExtraMargin; - final VoidCallback? onTap; + final FutureVoidCallback? onTap; final VoidCallback? onDoubleTap; final Color? menuItemColor; final bool alignCaptionedTextToLeft; - final double borderRadius; + + // singleBorderRadius is applied to the border when it's a standalone menu item. + // Widget will apply singleBorderRadius if value of both isTopBorderRadiusRemoved + // and isBottomBorderRadiusRemoved is false. Otherwise, multipleBorderRadius will + // be applied + final double singleBorderRadius; + final double multipleBorderRadius; final Color? pressedColor; final ExpandableController? expandableController; final bool isBottomBorderRadiusRemoved; @@ -38,6 +55,18 @@ class MenuItemWidget extends StatefulWidget { /// disable gesture detector if not used final bool isGestureDetectorDisabled; + ///Success state will not be shown if this flag is set to true, only idle and + ///loading state + final bool showOnlyLoadingState; + + final bool surfaceExecutionStates; + + ///To show success state even when execution time < debouce time, set this + ///flag to true. If the loading state needs to be shown and success state not, + ///set the showOnlyLoadingState flag to true, setting this flag to false won't + ///help. + final bool alwaysShowSuccessState; + const MenuItemWidget({ required this.captionedTextWidget, this.isExpandable = false, @@ -54,12 +83,16 @@ class MenuItemWidget extends StatefulWidget { this.onDoubleTap, this.menuItemColor, this.alignCaptionedTextToLeft = false, - this.borderRadius = 4.0, + this.singleBorderRadius = 4.0, + this.multipleBorderRadius = 8.0, this.pressedColor, this.expandableController, this.isBottomBorderRadiusRemoved = false, this.isTopBorderRadiusRemoved = false, this.isGestureDetectorDisabled = false, + this.showOnlyLoadingState = false, + this.surfaceExecutionStates = true, + this.alwaysShowSuccessState = false, Key? key, }) : super(key: key); @@ -68,11 +101,20 @@ class MenuItemWidget extends StatefulWidget { } class _MenuItemWidgetState extends State { + final _debouncer = Debouncer(const Duration(milliseconds: 300)); + ValueNotifier executionStateNotifier = + ValueNotifier(ExecutionState.idle); + Color? menuItemColor; + late double borderRadius; @override void initState() { menuItemColor = widget.menuItemColor; + borderRadius = + (widget.isBottomBorderRadiusRemoved || widget.isTopBorderRadiusRemoved) + ? widget.multipleBorderRadius + : widget.singleBorderRadius; if (widget.expandableController != null) { widget.expandableController!.addListener(() { setState(() {}); @@ -87,11 +129,18 @@ class _MenuItemWidgetState extends State { super.didChangeDependencies(); } + @override + void didUpdateWidget(covariant MenuItemWidget oldWidget) { + menuItemColor = widget.menuItemColor; + super.didUpdateWidget(oldWidget); + } + @override void dispose() { if (widget.expandableController != null) { widget.expandableController!.dispose(); } + executionStateNotifier.dispose(); super.dispose(); } @@ -100,7 +149,7 @@ class _MenuItemWidgetState extends State { return widget.isExpandable || widget.isGestureDetectorDisabled ? menuItemWidget(context) : GestureDetector( - onTap: widget.onTap, + onTap: _onTap, onDoubleTap: widget.onDoubleTap, onTapDown: _onTapDown, onTapUp: _onTapUp, @@ -110,16 +159,15 @@ class _MenuItemWidgetState extends State { } Widget menuItemWidget(BuildContext context) { - final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme; - final borderRadius = Radius.circular(widget.borderRadius); + final circularRadius = Radius.circular(borderRadius); final isExpanded = widget.expandableController?.value; final bottomBorderRadius = (isExpanded != null && isExpanded) || widget.isBottomBorderRadiusRemoved ? const Radius.circular(0) - : borderRadius; + : circularRadius; final topBorderRadius = widget.isTopBorderRadiusRemoved ? const Radius.circular(0) - : borderRadius; + : circularRadius; return AnimatedContainer( duration: const Duration(milliseconds: 20), width: double.infinity, @@ -138,67 +186,75 @@ class _MenuItemWidgetState extends State { children: [ widget.alignCaptionedTextToLeft && widget.leadingIcon == null ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.only(right: 10), - child: SizedBox( - height: widget.leadingIconSize, - width: widget.leadingIconSize, - child: widget.leadingIcon == null - ? (widget.leadingIconWidget != null - ? FittedBox( - fit: BoxFit.contain, - child: widget.leadingIconWidget, - ) - : const SizedBox.shrink()) - : FittedBox( - fit: BoxFit.contain, - child: Icon( - widget.leadingIcon, - color: widget.leadingIconColor ?? - enteColorScheme.strokeBase, - ), - ), - ), + : LeadingWidget( + leadingIconSize: widget.leadingIconSize, + leadingIcon: widget.leadingIcon, + leadingIconColor: widget.leadingIconColor, + leadingIconWidget: widget.leadingIconWidget, ), widget.captionedTextWidget, - widget.expandableController != null - ? AnimatedOpacity( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - opacity: isExpanded! ? 0 : 1, - child: AnimatedSwitcher( - transitionBuilder: (child, animation) { - return ScaleTransition(scale: animation, child: child); - }, - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOut, - child: isExpanded - ? const SizedBox.shrink() - : Icon( - widget.trailingIcon, - color: widget.trailingIconColor, - ), - ), - ) - : widget.trailingIcon != null - ? Padding( - padding: EdgeInsets.only( - right: widget.trailingExtraMargin, - ), - child: Icon( - widget.trailingIcon, - color: widget.trailingIconIsMuted - ? enteColorScheme.strokeMuted - : widget.trailingIconColor, - ), - ) - : widget.trailingWidget ?? const SizedBox.shrink(), + if (widget.expandableController != null) + ExpansionTrailingIcon( + isExpanded: isExpanded!, + trailingIcon: widget.trailingIcon, + trailingIconColor: widget.trailingIconColor, + ) + else + TrailingWidget( + executionStateNotifier: executionStateNotifier, + trailingIcon: widget.trailingIcon, + trailingIconColor: widget.trailingIconColor, + trailingWidget: widget.trailingWidget, + trailingIconIsMuted: widget.trailingIconIsMuted, + trailingExtraMargin: widget.trailingExtraMargin, + showExecutionStates: widget.surfaceExecutionStates, + key: ValueKey(widget.trailingIcon.hashCode), + ), ], ), ); } + Future _onTap() async { + if (executionStateNotifier.value == ExecutionState.inProgress || + executionStateNotifier.value == ExecutionState.successful) return; + _debouncer.run( + () => Future( + () { + executionStateNotifier.value = ExecutionState.inProgress; + }, + ), + ); + await widget.onTap?.call().then( + (value) { + widget.alwaysShowSuccessState + ? executionStateNotifier.value = ExecutionState.successful + : null; + }, + onError: (error, stackTrace) => _debouncer.cancelDebounce(), + ); + _debouncer.cancelDebounce(); + if (widget.alwaysShowSuccessState) { + Future.delayed(const Duration(seconds: 2), () { + executionStateNotifier.value = ExecutionState.idle; + }); + return; + } + if (executionStateNotifier.value == ExecutionState.inProgress) { + if (widget.showOnlyLoadingState) { + executionStateNotifier.value = ExecutionState.idle; + } else { + executionStateNotifier.value = ExecutionState.successful; + Future.delayed(const Duration(seconds: 2), () { + executionStateNotifier.value = ExecutionState.idle; + }); + } + } + } + void _onTapDown(details) { + if (executionStateNotifier.value == ExecutionState.inProgress || + executionStateNotifier.value == ExecutionState.successful) return; setState(() { if (widget.pressedColor == null) { hasPassedGestureCallbacks() @@ -215,6 +271,8 @@ class _MenuItemWidgetState extends State { } void _onTapUp(details) { + if (executionStateNotifier.value == ExecutionState.inProgress || + executionStateNotifier.value == ExecutionState.successful) return; Future.delayed( const Duration(milliseconds: 100), () => setState(() { @@ -224,6 +282,8 @@ class _MenuItemWidgetState extends State { } void _onCancel() { + if (executionStateNotifier.value == ExecutionState.inProgress || + executionStateNotifier.value == ExecutionState.successful) return; setState(() { menuItemColor = widget.menuItemColor; }); diff --git a/lib/ui/components/toggle_switch_widget.dart b/lib/ui/components/toggle_switch_widget.dart index 8968a9f78..7b6a82084 100644 --- a/lib/ui/components/toggle_switch_widget.dart +++ b/lib/ui/components/toggle_switch_widget.dart @@ -9,12 +9,12 @@ enum ExecutionState { successful, } -typedef FutureVoidCallBack = Future Function(); +typedef FutureVoidCallback = Future Function(); typedef BoolCallBack = bool Function(); class ToggleSwitchWidget extends StatefulWidget { final BoolCallBack value; - final FutureVoidCallBack onChanged; + final FutureVoidCallback onChanged; const ToggleSwitchWidget({ required this.value, required this.onChanged, diff --git a/lib/ui/create_collection_sheet.dart b/lib/ui/create_collection_sheet.dart index 32220f3e9..c2f5d7ab6 100644 --- a/lib/ui/create_collection_sheet.dart +++ b/lib/ui/create_collection_sheet.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:photos/core/configuration.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/models/collection_items.dart'; @@ -360,11 +361,17 @@ class _CreateCollectionSheetState extends State { } Future _addToCollection(int collectionID) async { - final dialog = createProgressDialog(context, "Uploading files to album..."); + final dialog = createProgressDialog( + context, + "Uploading files to album" + "...", + isDismissible: true, + ); await dialog.show(); try { final List files = []; final List filesPendingUpload = []; + final int currentUserID = Configuration.instance.getUserID()!; if (widget.sharedFiles != null) { filesPendingUpload.addAll( await convertIncomingSharedMediaToFile( @@ -374,8 +381,17 @@ class _CreateCollectionSheetState extends State { ); } else { for (final file in widget.selectedFiles!.files) { - final File? currentFile = - await (FilesDB.instance.getFile(file.generatedID!)); + File? currentFile; + if (file.uploadedFileID != null) { + currentFile = file; + } else if (file.generatedID != null) { + // when file is not uploaded, refresh the state from the db to + // ensure we have latest upload status for given file before + // queueing it up as pending upload + currentFile = await (FilesDB.instance.getFile(file.generatedID!)); + } else if (file.generatedID == null) { + _logger.severe("generated id should not be null"); + } if (currentFile == null) { _logger.severe("Failed to find fileBy genID"); continue; @@ -389,11 +405,20 @@ class _CreateCollectionSheetState extends State { } } if (filesPendingUpload.isNotEmpty) { - // filesPendingUpload might be getting ignored during auto-upload - // because the user deleted these files from ente in the past. - await IgnoredFilesService.instance - .removeIgnoredMappings(filesPendingUpload); - await FilesDB.instance.insertMultiple(filesPendingUpload); + // Newly created collection might not be cached + final Collection? c = + CollectionsService.instance.getCollectionByID(collectionID); + if (c != null && c.owner!.id != currentUserID) { + showToast(context, "Can not upload to albums owned by others"); + await dialog.hide(); + return false; + } else { + // filesPendingUpload might be getting ignored during auto-upload + // because the user deleted these files from ente in the past. + await IgnoredFilesService.instance + .removeIgnoredMappings(filesPendingUpload); + await FilesDB.instance.insertMultiple(filesPendingUpload); + } } if (files.isNotEmpty) { await CollectionsService.instance.addToCollection(collectionID, files); @@ -414,7 +439,7 @@ class _CreateCollectionSheetState extends State { final String message = widget.actionType == CollectionActionType.moveFiles ? "Moving files to album..." : "Unhiding files to album"; - final dialog = createProgressDialog(context, message); + final dialog = createProgressDialog(context, message, isDismissible: true); await dialog.show(); try { final int fromCollectionID = @@ -442,7 +467,8 @@ class _CreateCollectionSheetState extends State { } Future _restoreFilesToCollection(int toCollectionID) async { - final dialog = createProgressDialog(context, "Restoring files..."); + final dialog = createProgressDialog(context, "Restoring files...", + isDismissible: true); await dialog.show(); try { await CollectionsService.instance diff --git a/lib/ui/notification/update/change_log_page.dart b/lib/ui/notification/update/change_log_page.dart index 57f1cc169..5b9efe439 100644 --- a/lib/ui/notification/update/change_log_page.dart +++ b/lib/ui/notification/update/change_log_page.dart @@ -103,49 +103,34 @@ class _ChangeLogPageState extends State { final List items = []; items.add( ChangeLogEntry( - "Quick links!", - "Select some photos, choose \"Create link\" from the selection " - "options, and, well, that's it! You'll get a link that you can " - "share, end-to-end encrypted and secure.\n\nYour quick links will " - "appear at the bottom of the share tab so that you can remove them " - "when they're no longer needed, or convert them to regular albums " - "by renaming them if you want them to stick around.\n\nDepending on the feedback, we'll iterate on this (automatically prune quick " - "links, directly open the photo if only a single photo is shared, " - "etc). So let us know which direction you wish us to head!", + "Collaborative albums ✨", + "Much awaited, they're here now - create albums where multiple ente " + "users can add photos!\n\nWhen sharing an album, you can specify if" + " you want to add someone as a viewer or a collaborator. Collaborators can add photos " + "to the shared album.\n\nAlbums can have both collaborators and viewers, and as many as " + "you like. Storage is only counted once, for the person who uploaded the photo." + "\n\nHead over to the sharing options for an album to start adding collaborators.", ), ); items.add( ChangeLogEntry( - '''Filename search''', - "You can search for files by their names now.", + "Uncategorized", + "You can now keep photos that do not belong to a specific album." + "\n\nThis will simplify deletion and make it safer since now ente " + "will have a place to put photos that don't belong to any album " + "instead of always deleting them.\n\nThis will also allow you to " + "choose between keeping vs deleting photos present in the album, " + "when deleting an album.\n\nUncategorized photos can be seen from " + "the bottom of the albums tab.", ), ); items.add( ChangeLogEntry( - '''Prune empty albums''', - "There is now a button on the albums tab to remove all empty albums in one go. This will help customers with many empty albums clear out their clutter, and will be visible if you have more than 3 empty albums.", - ), - ); - items.add( - ChangeLogEntry( - '''Clear caches''', - "Under Settings > General > Advanced, you'll now see an option to " - "view and manage how ente uses temporary storage on your device." - "\n\nThe list will show a breakdown of cached files - Attaching a " - "screenshot of this would help if you feel the ente is using more" - " storage than expected.\n\nThere is also an option to clear all " - "these temporarily cached files to free up space on your device.", - ), - ); - - items.add( - ChangeLogEntry( - '''Reset ignored files''', - "We've added help text to clarify when a file in an on-device album " - "is ignored for backups because it was deleted from ente earlier," - " and an option to reset this state.\n\nWe've also fixed a bug " - "where an on-device album would get unmarked from backups after using the free up space option within it.", + '''Cleaner album picker''', + "Among other improvements, the list of albums that is shown when adding " + "or moving photos gets a facelift, and an issue causing the photo " + "zoom to be reset after loading the full resolution photo has been fixed.", isFeature: false, ), ); diff --git a/lib/ui/payment/billing_questions_widget.dart b/lib/ui/payment/billing_questions_widget.dart index b4ea481bb..52dc0bff7 100644 --- a/lib/ui/payment/billing_questions_widget.dart +++ b/lib/ui/payment/billing_questions_widget.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:expansion_tile_card/expansion_tile_card.dart'; import 'package:flutter/material.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/ui/common/loading_widget.dart'; @@ -14,7 +14,7 @@ class BillingQuestionsWidget extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( - future: Network.instance + future: NetworkClient.instance .getDio() .get("https://static.ente.io/faq.json") .then((response) { diff --git a/lib/ui/payment/child_subscription_widget.dart b/lib/ui/payment/child_subscription_widget.dart index 7ea3912ca..b80cbe356 100644 --- a/lib/ui/payment/child_subscription_widget.dart +++ b/lib/ui/payment/child_subscription_widget.dart @@ -118,7 +118,7 @@ class ChildSubscriptionWidget extends StatelessWidget { } Future _leaveFamilyPlan(BuildContext context) async { - final choice = await showNewChoiceDialog( + final choice = await showChoiceDialog( context, title: "Leave family", body: "Are you sure that you want to leave the family plan?", diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index a91ba652c..1328c31ea 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -344,7 +344,7 @@ class _StripeSubscriptionPageState extends State { onPressed: () async { bool confirmAction = false; if (isRenewCancelled) { - final choice = await showNewChoiceDialog( + final choice = await showChoiceDialog( context, title: title, body: "Are you sure you want to renew?", @@ -352,7 +352,7 @@ class _StripeSubscriptionPageState extends State { ); confirmAction = choice == ButtonAction.first; } else { - final choice = await showNewChoiceDialog( + final choice = await showChoiceDialog( context, title: title, body: "Are you sure you want to cancel?", @@ -429,7 +429,7 @@ class _StripeSubscriptionPageState extends State { String stripPurChaseAction = 'buy'; if (_isStripeSubscriber && _hasActiveSubscription) { // confirm if user wants to change plan or not - final result = await showNewChoiceDialog( + final result = await showChoiceDialog( context, title: "Confirm plan change", body: "Are you sure you want to change your plan?", diff --git a/lib/ui/settings/about_section_widget.dart b/lib/ui/settings/about_section_widget.dart index 3faae4985..297b558e6 100644 --- a/lib/ui/settings/about_section_widget.dart +++ b/lib/ui/settings/about_section_widget.dart @@ -4,7 +4,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/web_page.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/app_update_dialog.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -113,7 +113,7 @@ class AboutMenuItemWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { diff --git a/lib/ui/settings/account_section_widget.dart b/lib/ui/settings/account_section_widget.dart index 276647709..9a965100c 100644 --- a/lib/ui/settings/account_section_widget.dart +++ b/lib/ui/settings/account_section_widget.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:photos/ente_theme_data.dart'; import 'package:photos/services/local_authentication_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; @@ -12,7 +11,7 @@ import 'package:photos/ui/account/password_entry_page.dart'; import 'package:photos/ui/account/recovery_key_page.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; @@ -40,6 +39,7 @@ class AccountSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, + showOnlyLoadingState: true, onTap: () async { final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication( @@ -76,6 +76,7 @@ class AccountSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, + showOnlyLoadingState: true, onTap: () async { final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication( @@ -102,6 +103,7 @@ class AccountSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, + showOnlyLoadingState: true, onTap: () async { final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication( @@ -129,7 +131,7 @@ class AccountSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { _onLogoutTapped(context); }, ), @@ -141,7 +143,7 @@ class AccountSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { routeToPage(context, const DeleteAccountPage()); }, ), @@ -156,46 +158,14 @@ class AccountSectionWidget extends StatelessWidget { ); } - Future _onLogoutTapped(BuildContext context) async { - final AlertDialog alert = AlertDialog( - title: const Text( - "Logout", - style: TextStyle( - color: Colors.red, - ), - ), - content: const Text("Are you sure you want to logout?"), - actions: [ - TextButton( - child: const Text( - "Yes, logout", - style: TextStyle( - color: Colors.red, - ), - ), - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop('dialog'); - await UserService.instance.logout(context); - }, - ), - TextButton( - child: Text( - "No", - style: TextStyle( - color: Theme.of(context).colorScheme.greenAlternative, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - ], - ); - - await showDialog( - context: context, - builder: (BuildContext context) { - return alert; + void _onLogoutTapped(BuildContext context) { + showChoiceActionSheet( + context, + title: "Are you sure you want to logout?", + firstButtonLabel: "Yes, logout", + isCritical: true, + firstButtonOnTap: () async { + await UserService.instance.logout(context); }, ); } diff --git a/lib/ui/settings/app_update_dialog.dart b/lib/ui/settings/app_update_dialog.dart index 34954f9e5..29cb62cfd 100644 --- a/lib/ui/settings/app_update_dialog.dart +++ b/lib/ui/settings/app_update_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; // import 'package:open_file/open_file.dart'; import 'package:photos/core/configuration.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/theme/ente_theme.dart'; @@ -183,7 +183,7 @@ class _ApkDownloaderDialogState extends State { Future _downloadApk() async { try { - await Network.instance.getDio().download( + await NetworkClient.instance.getDio().download( widget.versionInfo!.url, _saveUrl, onReceiveProgress: (count, _) { diff --git a/lib/ui/settings/backup_section_widget.dart b/lib/ui/settings/backup_section_widget.dart index f29764e4c..415d01c93 100644 --- a/lib/ui/settings/backup_section_widget.dart +++ b/lib/ui/settings/backup_section_widget.dart @@ -12,7 +12,7 @@ import 'package:photos/ui/backup_settings_screen.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/ui/tools/deduplicate_page.dart'; @@ -49,7 +49,7 @@ class BackupSectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { routeToPage( context, const BackupFolderSelectionPage( @@ -66,7 +66,7 @@ class BackupSectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { routeToPage( context, const BackupSettingsScreen(), @@ -85,19 +85,16 @@ class BackupSectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, + showOnlyLoadingState: true, onTap: () async { - final dialog = createProgressDialog(context, "Calculating..."); - await dialog.show(); BackupStatus status; try { status = await SyncService.instance.getBackupStatus(); } catch (e) { - await dialog.hide(); showGenericErrorDialog(context: context); return; } - await dialog.hide(); if (status.localIDs.isEmpty) { showErrorDialog( context, @@ -121,20 +118,17 @@ class BackupSectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, + showOnlyLoadingState: true, onTap: () async { - final dialog = createProgressDialog(context, "Calculating..."); - await dialog.show(); List duplicates; try { duplicates = await DeduplicationService.instance.getDuplicateFiles(); } catch (e) { - await dialog.hide(); showGenericErrorDialog(context: context); return; } - await dialog.hide(); if (duplicates.isEmpty) { showErrorDialog( context, diff --git a/lib/ui/settings/debug_section_widget.dart b/lib/ui/settings/debug_section_widget.dart index c5b950dac..60f270661 100644 --- a/lib/ui/settings/debug_section_widget.dart +++ b/lib/ui/settings/debug_section_widget.dart @@ -8,7 +8,7 @@ import 'package:photos/services/update_service.dart'; import 'package:photos/theme/ente_theme.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/toast_util.dart'; diff --git a/lib/ui/settings/general_section_widget.dart b/lib/ui/settings/general_section_widget.dart index a3e73bf34..23b9eff89 100644 --- a/lib/ui/settings/general_section_widget.dart +++ b/lib/ui/settings/general_section_widget.dart @@ -5,10 +5,9 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/advanced_settings_screen.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/payment/subscription.dart'; import 'package:photos/ui/settings/common_settings.dart'; -import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; class GeneralSectionWidget extends StatelessWidget { @@ -34,7 +33,7 @@ class GeneralSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { _onManageSubscriptionTapped(context); }, ), @@ -46,8 +45,9 @@ class GeneralSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { - _onFamilyPlansTapped(context); + showOnlyLoadingState: true, + onTap: () async { + await _onFamilyPlansTapped(context); }, ), sectionOptionSpacing, @@ -58,7 +58,7 @@ class GeneralSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { _onAdvancedTapped(context); }, ), @@ -78,11 +78,8 @@ class GeneralSectionWidget extends StatelessWidget { } Future _onFamilyPlansTapped(BuildContext context) async { - final dialog = createProgressDialog(context, "Please wait..."); - await dialog.show(); final userDetails = await UserService.instance.getUserDetailsV2(memoryCount: false); - await dialog.hide(); BillingService.instance.launchFamilyPortal(context, userDetails); } diff --git a/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index e4d7b746e..e5c16452b 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/lib/ui/settings/security_section_widget.dart @@ -11,7 +11,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/account/sessions_page.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; @@ -119,6 +119,7 @@ class _SecuritySectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, + showOnlyLoadingState: true, onTap: () async { final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication( diff --git a/lib/ui/settings/social_section_widget.dart b/lib/ui/settings/social_section_widget.dart index 801718d00..43572c7a3 100644 --- a/lib/ui/settings/social_section_widget.dart +++ b/lib/ui/settings/social_section_widget.dart @@ -3,7 +3,7 @@ import 'package:photos/services/update_service.dart'; import 'package:photos/theme/ente_theme.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -69,7 +69,7 @@ class SocialsMenuItemWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { launchUrlString(urlSring); }, ); diff --git a/lib/ui/settings/support_section_widget.dart b/lib/ui/settings/support_section_widget.dart index cbd2458a5..dd26ab124 100644 --- a/lib/ui/settings/support_section_widget.dart +++ b/lib/ui/settings/support_section_widget.dart @@ -7,7 +7,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/web_page.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/about_section_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/email_util.dart'; @@ -54,7 +54,7 @@ class SupportSectionWidget extends StatelessWidget { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () { + onTap: () async { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { diff --git a/lib/ui/settings/theme_switch_widget.dart b/lib/ui/settings/theme_switch_widget.dart index 2b84a0e54..a63ca37b0 100644 --- a/lib/ui/settings/theme_switch_widget.dart +++ b/lib/ui/settings/theme_switch_widget.dart @@ -5,7 +5,7 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/theme/ente_theme.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/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; class ThemeSwitchWidget extends StatefulWidget { diff --git a/lib/ui/shared_collections_gallery.dart b/lib/ui/shared_collections_gallery.dart index ac907228b..2820e60b4 100644 --- a/lib/ui/shared_collections_gallery.dart +++ b/lib/ui/shared_collections_gallery.dart @@ -11,6 +11,7 @@ import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/events/tab_changed_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; +import 'package:photos/models/collection.dart'; import 'package:photos/models/collection_items.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/services/collections_service.dart'; @@ -69,12 +70,22 @@ class _SharedCollectionGalleryState extends State final List outgoing = []; final List incoming = []; for (final file in files) { - final c = CollectionsService.instance - .getCollectionByID(file.collectionID!)!; + if (file.collectionID == null) { + _logger.severe("collection id should not be null"); + continue; + } + final Collection? c = + CollectionsService.instance.getCollectionByID(file.collectionID!); + if (c == null) { + _logger + .severe("shared collection is not cached ${file.collectionID}"); + CollectionsService.instance + .fetchCollectionByID(file.collectionID!) + .ignore(); + continue; + } if (c.owner!.id == Configuration.instance.getUserID()) { - if (c.sharees!.isNotEmpty || - c.publicURLs!.isNotEmpty || - c.isSharedFilesCollection()) { + if (c.hasSharees || c.hasLink || c.isSharedFilesCollection()) { outgoing.add( CollectionWithThumbnail( c, @@ -113,8 +124,12 @@ class _SharedCollectionGalleryState extends State if (snapshot.hasData) { return _getSharedCollectionsGallery(snapshot.data!); } else if (snapshot.hasError) { - _logger.shout(snapshot.error); - return Center(child: Text(snapshot.error.toString())); + _logger.severe( + "critical: failed to load share gallery", + snapshot.error, + snapshot.stackTrace, + ); + return const Center(child: Text("Something went wrong.")); } else { return const EnteLoadingWidget(); } @@ -268,27 +283,29 @@ class OutgoingCollectionItem extends StatelessWidget { @override Widget build(BuildContext context) { - final sharees = []; - for (int index = 0; index < c.collection.sharees!.length; index++) { - final sharee = c.collection.sharees![index]!; - final name = - (sharee.name?.isNotEmpty ?? false) ? sharee.name : sharee.email; - if (index < 2) { - sharees.add(name); - } else { - final remaining = c.collection.sharees!.length - index; - if (remaining == 1) { - // If it's the last sharee - sharees.add(name); + final shareesName = []; + if (c.collection.hasSharees) { + for (int index = 0; index < c.collection.sharees!.length; index++) { + final sharee = c.collection.sharees![index]!; + final String name = + (sharee.name?.isNotEmpty ?? false) ? sharee.name! : sharee.email; + if (index < 2) { + shareesName.add(name); } else { - sharees.add( - "and " + - remaining.toString() + - " other" + - (remaining > 1 ? "s" : ""), - ); + final remaining = c.collection.sharees!.length - index; + if (remaining == 1) { + // If it's the last sharee + shareesName.add(name); + } else { + shareesName.add( + "and " + + remaining.toString() + + " other" + + (remaining > 1 ? "s" : ""), + ); + } + break; } - break; } } return GestureDetector( @@ -325,22 +342,22 @@ class OutgoingCollectionItem extends StatelessWidget { ), ), const Padding(padding: EdgeInsets.all(2)), - c.collection.publicURLs!.isEmpty - ? Container() - : (c.collection.publicURLs!.first!.isExpired + c.collection.hasLink + ? (c.collection.publicURLs!.first!.isExpired ? const Icon( Icons.link, color: warning500, ) - : const Icon(Icons.link)), + : const Icon(Icons.link)) + : Container(), ], ), - sharees.isEmpty + shareesName.isEmpty ? Container() : Padding( padding: const EdgeInsets.fromLTRB(0, 4, 0, 0), child: Text( - "Shared with " + sharees.join(", "), + "Shared with " + shareesName.join(", "), style: TextStyle( fontSize: 14, color: Theme.of(context).primaryColorLight, diff --git a/lib/ui/sharing/add_partipant_page.dart b/lib/ui/sharing/add_partipant_page.dart index 2368e025e..5c1ae2554 100644 --- a/lib/ui/sharing/add_partipant_page.dart +++ b/lib/ui/sharing/add_partipant_page.dart @@ -1,34 +1,33 @@ import 'package:email_validator/email_validator.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; -import 'package:photos/ui/common/gradient_button.dart'; +import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; +import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/sharing/user_avator_widget.dart'; -import 'package:photos/utils/toast_util.dart'; class AddParticipantPage extends StatefulWidget { final Collection collection; + final bool isAddingViewer; - const AddParticipantPage(this.collection, {super.key}); + const AddParticipantPage(this.collection, this.isAddingViewer, {super.key}); @override State createState() => _AddParticipantPage(); } class _AddParticipantPage extends State { - late bool selectAsViewer; String selectedEmail = ''; String _email = ''; - bool hideListOfEmails = false; + bool isEmailListEmpty = false; bool _emailIsValid = false; bool isKeypadOpen = false; late CollectionActions collectionActions; @@ -39,7 +38,6 @@ class _AddParticipantPage extends State { @override void initState() { - selectAsViewer = true; collectionActions = CollectionActions(CollectionsService.instance); super.initState(); } @@ -54,12 +52,13 @@ class _AddParticipantPage extends State { Widget build(BuildContext context) { isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; final enteTextTheme = getEnteTextTheme(context); + final enteColorScheme = getEnteColorScheme(context); final List suggestedUsers = _getSuggestedUser(); - hideListOfEmails = suggestedUsers.isEmpty; + isEmailListEmpty = suggestedUsers.isEmpty; return Scaffold( resizeToAvoidBottomInset: isKeypadOpen, appBar: AppBar( - title: const Text("Add people"), + title: Text(widget.isAddingViewer ? "Add viewer" : "Add collaborator"), ), body: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -70,7 +69,8 @@ class _AddParticipantPage extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( "Add a new email", - style: enteTextTheme.body, + style: enteTextTheme.small + .copyWith(color: enteColorScheme.textMuted), ), ), const SizedBox(height: 4), @@ -78,20 +78,32 @@ class _AddParticipantPage extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: _getEmailField(), ), - (hideListOfEmails || isKeypadOpen) - ? const Expanded(child: SizedBox()) + (isEmailListEmpty && widget.isAddingViewer) + ? const Expanded(child: SizedBox.shrink()) : Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ - const SizedBox(height: 24), - const MenuSectionTitle( - title: "or pick an existing one", - ), + !isEmailListEmpty + ? const MenuSectionTitle( + title: "Or pick an existing one", + ) + : const SizedBox.shrink(), Expanded( child: ListView.builder( itemBuilder: (context, index) { + if (index >= suggestedUsers.length) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + child: MenuSectionDescriptionWidget( + content: + "Collaborators can add photos and videos to the shared album.", + ), + ); + } final currentUser = suggestedUsers[index]; return Column( children: [ @@ -137,8 +149,8 @@ class _AddParticipantPage extends State { ], ); }, - itemCount: suggestedUsers.length, - + itemCount: suggestedUsers.length + + (widget.isAddingViewer ? 0 : 1), // physics: const ClampingScrollPhysics(), ), ), @@ -146,9 +158,6 @@ class _AddParticipantPage extends State { ), ), ), - const DividerWidget( - dividerType: DividerType.solid, - ), SafeArea( child: Padding( padding: const EdgeInsets.only( @@ -160,74 +169,34 @@ class _AddParticipantPage extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const MenuSectionTitle(title: "Add as"), - MenuItemWidget( - captionedTextWidget: const CaptionedTextWidget( - title: "Collaborator", - ), - leadingIcon: Icons.edit_outlined, - menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: !selectAsViewer ? Icons.check : null, - onTap: () async { - if (kDebugMode) { - setState(() => {selectAsViewer = false}); - } else { - showShortToast(context, "Coming soon..."); - } - }, - isBottomBorderRadiusRemoved: true, - ), - DividerWidget( - dividerType: DividerType.menu, - bgColor: getEnteColorScheme(context).fillFaint, - ), - MenuItemWidget( - captionedTextWidget: const CaptionedTextWidget( - title: "Viewer", - ), - leadingIcon: Icons.photo_outlined, - menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: selectAsViewer ? Icons.check : null, - onTap: () async { - setState(() => {selectAsViewer = true}); - // showShortToast(context, "yet to implement"); - }, - isTopBorderRadiusRemoved: true, - ), - !isKeypadOpen - ? const MenuSectionDescriptionWidget( - content: - "Collaborators can add photos and videos to the shared album.", - ) - : const SizedBox.shrink(), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: GradientButton( - onTap: (selectedEmail == '' && !_emailIsValid) - ? null - : () async { - final emailToAdd = - selectedEmail == '' ? _email : selectedEmail; - final result = - await collectionActions.addEmailToCollection( - context, - widget.collection, - emailToAdd, - role: selectAsViewer - ? CollectionParticipantRole.viewer - : CollectionParticipantRole.collaborator, - ); - if (result != null && result && mounted) { - Navigator.of(context).pop(true); - } - }, - text: selectAsViewer ? "Add viewer" : "Add collaborator", - ), - ), const SizedBox(height: 8), + ButtonWidget( + buttonType: ButtonType.primary, + buttonSize: ButtonSize.large, + labelText: widget.isAddingViewer + ? "Add viewer" + : "Add collaborator", + isDisabled: (selectedEmail == '' && !_emailIsValid), + onTap: (selectedEmail == '' && !_emailIsValid) + ? null + : () async { + final emailToAdd = + selectedEmail == '' ? _email : selectedEmail; + final result = + await collectionActions.addEmailToCollection( + context, + widget.collection, + emailToAdd, + widget.isAddingViewer + ? CollectionParticipantRole.viewer + : CollectionParticipantRole.collaborator, + ); + if (result && mounted) { + Navigator.of(context).pop(true); + } + }, + ), + const SizedBox(height: 20), ], ), ), @@ -287,13 +256,8 @@ class _AddParticipantPage extends State { selectedEmail = ''; } _email = value.trim(); - if (_emailIsValid != EmailValidator.validate(_email)) { - setState(() { - _emailIsValid = EmailValidator.validate(_email); - }); - } else if (_email.length < 2) { - setState(() {}); - } + _emailIsValid = EmailValidator.validate(_email); + setState(() {}); }, autocorrect: false, keyboardType: TextInputType.emailAddress, @@ -326,7 +290,15 @@ class _AddParticipantPage extends State { suggestedUsers.add(c.owner!); } } + if (_textController.text.trim().isNotEmpty) { + suggestedUsers.removeWhere( + (element) => !element.email + .toLowerCase() + .contains(_textController.text.trim().toLowerCase()), + ); + } suggestedUsers.sort((a, b) => a.email.compareTo(b.email)); + return suggestedUsers; } } diff --git a/lib/ui/sharing/album_participants_page.dart b/lib/ui/sharing/album_participants_page.dart index 4da72b31a..bf7b79908 100644 --- a/lib/ui/sharing/album_participants_page.dart +++ b/lib/ui/sharing/album_participants_page.dart @@ -5,7 +5,7 @@ import 'package:photos/models/collection.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; @@ -51,7 +51,7 @@ class _AlbumParticipantsPageState extends State { Future _navigateToAddUser(bool addingViewer) async { await routeToPage( context, - AddParticipantPage(widget.collection), + AddParticipantPage(widget.collection, addingViewer), ); if (mounted) { setState(() => {}); @@ -72,7 +72,9 @@ class _AlbumParticipantsPageState extends State { final splitResult = widget.collection.getSharees().splitMatch((x) => x.isViewer); final List viewers = splitResult.matched; + viewers.sort((a, b) => a.email.compareTo(b.email)); final List collaborators = splitResult.unmatched; + collaborators.sort((a, b) => a.email.compareTo(b.email)); return Scaffold( body: CustomScrollView( @@ -110,7 +112,7 @@ class _AlbumParticipantsPageState extends State { ), leadingIconSize: 24, menuItemColor: colorScheme.fillFaint, - borderRadius: 8, + singleBorderRadius: 8, isGestureDetectorDisabled: true, ), ], @@ -153,17 +155,18 @@ class _AlbumParticipantsPageState extends State { currentUserID: currentUserID, ), menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: isOwner ? Icons.chevron_right : null, trailingIconIsMuted: true, - onTap: () async { - if (isOwner) { - await _navigateToManageUser(currentUser); - } - }, + onTap: isOwner + ? () async { + if (isOwner) { + _navigateToManageUser(currentUser); + } + } + : null, isTopBorderRadiusRemoved: listIndex > 0, isBottomBorderRadiusRemoved: true, - borderRadius: 8, + singleBorderRadius: 8, ), DividerWidget( dividerType: DividerType.menu, @@ -174,18 +177,18 @@ class _AlbumParticipantsPageState extends State { } else if (index == (1 + collaborators.length) && isOwner) { return MenuItemWidget( captionedTextWidget: CaptionedTextWidget( - title: - collaborators.isNotEmpty ? "Add more" : "Add email", + title: collaborators.isNotEmpty + ? "Add more" + : "Add collaborator", makeTextBold: true, ), leadingIcon: Icons.add_outlined, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, onTap: () async { - await _navigateToAddUser(false); + _navigateToAddUser(false); }, isTopBorderRadiusRemoved: collaborators.isNotEmpty, - borderRadius: 8, + singleBorderRadius: 8, ); } return const SizedBox.shrink(); @@ -226,17 +229,18 @@ class _AlbumParticipantsPageState extends State { currentUserID: currentUserID, ), menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: isOwner ? Icons.chevron_right : null, trailingIconIsMuted: true, - onTap: () async { - if (isOwner) { - await _navigateToManageUser(currentUser); - } - }, + onTap: isOwner + ? () async { + if (isOwner) { + await _navigateToManageUser(currentUser); + } + } + : null, isTopBorderRadiusRemoved: listIndex > 0, isBottomBorderRadiusRemoved: !isLastItem, - borderRadius: 8, + singleBorderRadius: 8, ), isLastItem ? const SizedBox.shrink() @@ -249,17 +253,16 @@ class _AlbumParticipantsPageState extends State { } else if (index == (1 + viewers.length) && isOwner) { return MenuItemWidget( captionedTextWidget: CaptionedTextWidget( - title: viewers.isNotEmpty ? "Add more" : "Add Viewer", + title: viewers.isNotEmpty ? "Add more" : "Add viewer", makeTextBold: true, ), leadingIcon: Icons.add_outlined, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, onTap: () async { - await _navigateToAddUser(true); + _navigateToAddUser(true); }, isTopBorderRadiusRemoved: viewers.isNotEmpty, - borderRadius: 8, + singleBorderRadius: 8, ); } return const SizedBox.shrink(); diff --git a/lib/ui/sharing/manage_album_participant.dart b/lib/ui/sharing/manage_album_participant.dart index 532652a9c..e3c8340a9 100644 --- a/lib/ui/sharing/manage_album_participant.dart +++ b/lib/ui/sharing/manage_album_participant.dart @@ -1,17 +1,17 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; +import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; -import 'package:photos/utils/toast_util.dart'; +import 'package:photos/utils/dialog_util.dart'; class ManageIndividualParticipant extends StatefulWidget { final Collection collection; @@ -36,6 +36,7 @@ class _ManageIndividualParticipantState Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); final textTheme = getEnteTextTheme(context); + bool isConvertToViewSuccess = false; return Scaffold( appBar: AppBar(), body: Padding( @@ -71,23 +72,18 @@ class _ManageIndividualParticipantState ), leadingIcon: Icons.edit_outlined, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: widget.user.isCollaborator ? Icons.check : null, onTap: widget.user.isCollaborator ? null : () async { - if (!kDebugMode) { - showShortToast(context, "Coming soon..."); - return; - } final result = await collectionActions.addEmailToCollection( context, widget.collection, widget.user.email, - role: CollectionParticipantRole.collaborator, + CollectionParticipantRole.collaborator, ); - if ((result ?? false) && mounted) { + if (result && mounted) { widget.user.role = CollectionParticipantRole .collaborator .toStringVal(); @@ -107,22 +103,40 @@ class _ManageIndividualParticipantState leadingIcon: Icons.photo_outlined, leadingIconColor: getEnteColorScheme(context).strokeBase, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: widget.user.isViewer ? Icons.check : null, + showOnlyLoadingState: true, onTap: widget.user.isViewer ? null : () async { - final result = - await collectionActions.addEmailToCollection( + final ButtonAction? result = await showChoiceActionSheet( context, - widget.collection, - widget.user.email, - role: CollectionParticipantRole.viewer, + title: "Change permissions?", + firstButtonLabel: "Yes, convert to viewer", + body: + '${widget.user.email} will not be able to add more photos to this album\n\nThey will still be able to remove existing photos added by them', + isCritical: true, ); - if ((result ?? false) && mounted) { - widget.user.role = - CollectionParticipantRole.viewer.toStringVal(); - setState(() => {}); + if (result != null) { + if (result == ButtonAction.first) { + try { + isConvertToViewSuccess = + await collectionActions.addEmailToCollection( + context, + widget.collection, + widget.user.email, + CollectionParticipantRole.viewer, + ); + } catch (e) { + showGenericErrorDialog(context: context); + } + if (isConvertToViewSuccess && mounted) { + // reset value + isConvertToViewSuccess = false; + widget.user.role = + CollectionParticipantRole.viewer.toStringVal(); + setState(() => {}); + } + } } }, isTopBorderRadiusRemoved: true, @@ -142,7 +156,7 @@ class _ManageIndividualParticipantState leadingIcon: Icons.not_interested_outlined, leadingIconColor: warning500, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, + surfaceExecutionStates: false, onTap: () async { final result = await collectionActions.removeParticipant( context, @@ -150,7 +164,7 @@ class _ManageIndividualParticipantState widget.user, ); - if ((result ?? false) && mounted) { + if ((result) && mounted) { Navigator.of(context).pop(true); } }, diff --git a/lib/ui/sharing/manage_links_widget.dart b/lib/ui/sharing/manage_links_widget.dart index aa9237bf7..22b13df2b 100644 --- a/lib/ui/sharing/manage_links_widget.dart +++ b/lib/ui/sharing/manage_links_widget.dart @@ -4,9 +4,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; @@ -14,13 +12,15 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; +import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart'; +import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/date_time_util.dart'; import 'package:photos/utils/dialog_util.dart'; +import 'package:photos/utils/navigation_util.dart'; import 'package:photos/utils/toast_util.dart'; -import 'package:tuple/tuple.dart'; class ManageSharedLinkWidget extends StatefulWidget { final Collection? collection; @@ -32,26 +32,11 @@ class ManageSharedLinkWidget extends StatefulWidget { } class _ManageSharedLinkWidgetState extends State { - // index, title, milliseconds in future post which link should expire (when >0) - final List> _expiryOptions = [ - const Tuple3(0, "Never", 0), - Tuple3(1, "After 1 hour", const Duration(hours: 1).inMicroseconds), - Tuple3(2, "After 1 day", const Duration(days: 1).inMicroseconds), - Tuple3(3, "After 1 week", const Duration(days: 7).inMicroseconds), - // todo: make this time calculation perfect - Tuple3(4, "After 1 month", const Duration(days: 30).inMicroseconds), - Tuple3(5, "After 1 year", const Duration(days: 365).inMicroseconds), - const Tuple3(6, "Custom", -1), - ]; - - late Tuple3 _selectedExpiry; - int _selectedDeviceLimitIndex = 0; final CollectionActions sharingActions = CollectionActions(CollectionsService.instance); @override void initState() { - _selectedExpiry = _expiryOptions.first; super.initState(); } @@ -81,7 +66,6 @@ class _ManageSharedLinkWidgetState extends State { ), alignCaptionedTextToLeft: true, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingWidget: Switch.adaptive( value: widget.collection!.publicURLs?.firstOrNull ?.enableCollect ?? @@ -113,8 +97,14 @@ class _ManageSharedLinkWidgetState extends State { ), trailingIcon: Icons.chevron_right, menuItemColor: enteColorScheme.fillFaint, + surfaceExecutionStates: false, onTap: () async { - await showPicker(); + routeToPage( + context, + LinkExpiryPickerPage(widget.collection!), + ).then((value) { + setState(() {}); + }); }, ), url.hasExpiry @@ -138,8 +128,14 @@ class _ManageSharedLinkWidgetState extends State { alignCaptionedTextToLeft: true, isBottomBorderRadiusRemoved: true, onTap: () async { - await _showDeviceLimitPicker(); + routeToPage( + context, + DeviceLimitPickerPage(widget.collection!), + ).then((value) { + setState(() {}); + }); }, + surfaceExecutionStates: false, ), DividerWidget( dividerType: DividerType.menuNoIcon, @@ -153,7 +149,6 @@ class _ManageSharedLinkWidgetState extends State { isBottomBorderRadiusRemoved: true, isTopBorderRadiusRemoved: true, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingWidget: Switch.adaptive( value: widget.collection!.publicURLs?.firstOrNull ?.enableDownload ?? @@ -185,7 +180,6 @@ class _ManageSharedLinkWidgetState extends State { alignCaptionedTextToLeft: true, isTopBorderRadiusRemoved: true, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingWidget: Switch.adaptive( value: widget.collection!.publicURLs?.firstOrNull ?.passwordEnabled ?? @@ -224,16 +218,14 @@ class _ManageSharedLinkWidgetState extends State { leadingIcon: Icons.remove_circle_outline, leadingIconColor: warning500, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, + surfaceExecutionStates: false, onTap: () async { - final bool result = await sharingActions.publicLinkToggle( + final bool result = await sharingActions.disableUrl( context, widget.collection!, - false, ); if (result && mounted) { Navigator.of(context).pop(); - // setState(() => {}); } }, ), @@ -246,153 +238,6 @@ class _ManageSharedLinkWidgetState extends State { ); } - Future showPicker() async { - return showCupertinoModalPopup( - context: context, - builder: (context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.cupertinoPickerTopColor, - border: const Border( - bottom: BorderSide( - color: Color(0xff999999), - width: 0.0, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CupertinoButton( - onPressed: () { - Navigator.of(context).pop('cancel'); - }, - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 5.0, - ), - child: Text( - 'Cancel', - style: Theme.of(context).textTheme.subtitle1, - ), - ), - CupertinoButton( - onPressed: () async { - int newValidTill = -1; - bool hasSelectedCustom = false; - final int expireAfterInMicroseconds = - _selectedExpiry.item3; - // need to manually select time - if (expireAfterInMicroseconds < 0) { - hasSelectedCustom = true; - Navigator.of(context).pop(''); - final timeInMicrosecondsFromEpoch = - await _showDateTimePicker(); - if (timeInMicrosecondsFromEpoch != null) { - newValidTill = timeInMicrosecondsFromEpoch; - } - } else if (expireAfterInMicroseconds == 0) { - // no expiry - newValidTill = 0; - } else { - newValidTill = DateTime.now().microsecondsSinceEpoch + - expireAfterInMicroseconds; - } - if (!hasSelectedCustom) { - Navigator.of(context).pop(''); - } - if (newValidTill >= 0) { - debugPrint("Setting expirty $newValidTill"); - await updateTime(newValidTill); - } - }, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 2.0, - ), - child: Text( - 'Confirm', - style: Theme.of(context).textTheme.subtitle1, - ), - ) - ], - ), - ), - Container( - height: 220.0, - color: const Color(0xfff7f7f7), - child: CupertinoPicker( - backgroundColor: - Theme.of(context).backgroundColor.withOpacity(0.95), - onSelectedItemChanged: (value) { - final firstWhere = _expiryOptions - .firstWhere((element) => element.item1 == value); - setState(() { - _selectedExpiry = firstWhere; - }); - }, - magnification: 1.3, - useMagnifier: true, - itemExtent: 25, - diameterRatio: 1, - children: _expiryOptions - .map( - (e) => Text( - e.item2, - style: Theme.of(context).textTheme.subtitle1, - ), - ) - .toList(), - ), - ) - ], - ); - }, - ); - } - - Future updateTime(int newValidTill) async { - await _updateUrlSettings( - context, - {'validTill': newValidTill}, - ); - if (mounted) { - // reset to default value. THis is needed will we move to - // new selection menu as per figma/ - _selectedExpiry = _expiryOptions.first; - setState(() {}); - } - } - - // _showDateTimePicker return null if user doesn't select date-time - Future _showDateTimePicker() async { - final dateResult = await DatePicker.showDatePicker( - context, - minTime: DateTime.now(), - currentTime: DateTime.now(), - locale: LocaleType.en, - theme: Theme.of(context).colorScheme.dateTimePickertheme, - ); - if (dateResult == null) { - return null; - } - final dateWithTimeResult = await DatePicker.showTime12hPicker( - context, - showTitleActions: true, - currentTime: dateResult, - locale: LocaleType.en, - theme: Theme.of(context).colorScheme.dateTimePickertheme, - ); - if (dateWithTimeResult == null) { - return null; - } else { - return dateWithTimeResult.microsecondsSinceEpoch; - } - } - final TextEditingController _textFieldController = TextEditingController(); Future _displayLinkPasswordInput(BuildContext context) async { @@ -497,87 +342,4 @@ class _ManageSharedLinkWidgetState extends State { await showGenericErrorDialog(context: context); } } - - Future _showDeviceLimitPicker() async { - final List options = []; - for (int i = 50; i > 0; i--) { - options.add( - Text(i.toString(), style: Theme.of(context).textTheme.subtitle1), - ); - } - return showCupertinoModalPopup( - context: context, - builder: (context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.cupertinoPickerTopColor, - border: const Border( - bottom: BorderSide( - color: Color(0xff999999), - width: 0.0, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CupertinoButton( - onPressed: () { - Navigator.of(context).pop('cancel'); - }, - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 5.0, - ), - child: Text( - 'Cancel', - style: Theme.of(context).textTheme.subtitle1, - ), - ), - CupertinoButton( - onPressed: () async { - await _updateUrlSettings(context, { - 'deviceLimit': int.tryParse( - options[_selectedDeviceLimitIndex].data!, - ), - }); - setState(() {}); - Navigator.of(context).pop(''); - }, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 2.0, - ), - child: Text( - 'Confirm', - style: Theme.of(context).textTheme.subtitle1, - ), - ) - ], - ), - ), - Container( - height: 220.0, - color: const Color(0xfff7f7f7), - child: CupertinoPicker( - backgroundColor: - Theme.of(context).backgroundColor.withOpacity(0.95), - onSelectedItemChanged: (value) { - _selectedDeviceLimitIndex = value; - }, - magnification: 1.3, - useMagnifier: true, - itemExtent: 25, - diameterRatio: 1, - children: options, - ), - ) - ], - ); - }, - ); - } } diff --git a/lib/ui/sharing/pickers/device_limit_picker_page.dart b/lib/ui/sharing/pickers/device_limit_picker_page.dart new file mode 100644 index 000000000..8a9430773 --- /dev/null +++ b/lib/ui/sharing/pickers/device_limit_picker_page.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:photos/core/constants.dart'; +import 'package:photos/models/collection.dart'; +import 'package:photos/services/collections_service.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/captioned_text_widget.dart'; +import 'package:photos/ui/components/divider_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_section_description_widget.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import 'package:photos/utils/dialog_util.dart'; +import 'package:photos/utils/separators_util.dart'; + +class DeviceLimitPickerPage extends StatelessWidget { + final Collection collection; + const DeviceLimitPickerPage(this.collection, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + const TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: "Device Limit", + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: ItemsWidget(collection), + ), + const MenuSectionDescriptionWidget( + content: + "When set to the maximum (50), the device limit will be relaxed" + " to allow for temporary spikes of large number of viewers.", + ) + ], + ), + ); + }, + childCount: 1, + ), + ), + const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), + ], + ), + ); + } +} + +class ItemsWidget extends StatefulWidget { + final Collection collection; + const ItemsWidget(this.collection, {super.key}); + + @override + State createState() => _ItemsWidgetState(); +} + +class _ItemsWidgetState extends State { + late int currentDeviceLimit; + late int initialDeviceLimit; + List items = []; + bool isCustomLimit = false; + @override + void initState() { + currentDeviceLimit = widget.collection.publicURLs!.first!.deviceLimit; + initialDeviceLimit = currentDeviceLimit; + if (!publicLinkDeviceLimits.contains(currentDeviceLimit)) { + isCustomLimit = true; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + items.clear(); + if (isCustomLimit) { + items.add( + _menuItemForPicker(initialDeviceLimit), + ); + } + for (int deviceLimit in publicLinkDeviceLimits) { + items.add( + _menuItemForPicker(deviceLimit), + ); + } + items = addSeparators( + items, + DividerWidget( + dividerType: DividerType.menuNoIcon, + bgColor: getEnteColorScheme(context).fillFaint, + ), + ); + return Column( + mainAxisSize: MainAxisSize.min, + children: items, + ); + } + + Widget _menuItemForPicker(int deviceLimit) { + return MenuItemWidget( + key: ValueKey(deviceLimit), + menuItemColor: getEnteColorScheme(context).fillFaint, + captionedTextWidget: CaptionedTextWidget( + title: "$deviceLimit", + ), + trailingIcon: currentDeviceLimit == deviceLimit ? Icons.check : null, + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: true, + isBottomBorderRadiusRemoved: true, + showOnlyLoadingState: true, + onTap: () async { + await _updateUrlSettings(context, { + 'deviceLimit': deviceLimit, + }).then( + (value) => setState(() { + currentDeviceLimit = deviceLimit; + }), + ); + }, + ); + } + + Future _updateUrlSettings( + BuildContext context, + Map prop, + ) async { + try { + await CollectionsService.instance.updateShareUrl(widget.collection, prop); + } catch (e) { + showGenericErrorDialog(context: context); + rethrow; + } + } +} diff --git a/lib/ui/sharing/pickers/link_expiry_picker_page.dart b/lib/ui/sharing/pickers/link_expiry_picker_page.dart new file mode 100644 index 000000000..7c86e437a --- /dev/null +++ b/lib/ui/sharing/pickers/link_expiry_picker_page.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; +import 'package:photos/ente_theme_data.dart'; +import 'package:photos/models/collection.dart'; +import 'package:photos/services/collections_service.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/captioned_text_widget.dart'; +import 'package:photos/ui/components/divider_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import 'package:photos/utils/dialog_util.dart'; +import 'package:photos/utils/separators_util.dart'; +import 'package:tuple/tuple.dart'; + +class LinkExpiryPickerPage extends StatelessWidget { + final Collection collection; + const LinkExpiryPickerPage(this.collection, {super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + const TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: "Link expiry", + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: ItemsWidget(collection), + ), + ], + ), + ); + }, + childCount: 1, + ), + ), + const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), + ], + ), + ); + } +} + +class ItemsWidget extends StatelessWidget { + final Collection collection; + ItemsWidget(this.collection, {super.key}); + + // index, title, milliseconds in future post which link should expire (when >0) + final List> _expiryOptions = [ + const Tuple2("Never", 0), + Tuple2("After 1 hour", const Duration(hours: 1).inMicroseconds), + Tuple2("After 1 day", const Duration(days: 1).inMicroseconds), + Tuple2("After 1 week", const Duration(days: 7).inMicroseconds), + // todo: make this time calculation perfect + Tuple2("After 1 month", const Duration(days: 30).inMicroseconds), + Tuple2("After 1 year", const Duration(days: 365).inMicroseconds), + const Tuple2("Custom", -1), + ]; + + @override + Widget build(BuildContext context) { + List items = []; + for (Tuple2 expiryOpiton in _expiryOptions) { + items.add( + _menuItemForPicker(context, expiryOpiton), + ); + } + items = addSeparators( + items, + DividerWidget( + dividerType: DividerType.menuNoIcon, + bgColor: getEnteColorScheme(context).fillFaint, + ), + ); + return Column( + mainAxisSize: MainAxisSize.min, + children: items, + ); + } + + Widget _menuItemForPicker( + BuildContext context, + Tuple2 expiryOpiton, + ) { + return MenuItemWidget( + menuItemColor: getEnteColorScheme(context).fillFaint, + captionedTextWidget: CaptionedTextWidget( + title: expiryOpiton.item1, + ), + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: true, + isBottomBorderRadiusRemoved: true, + alwaysShowSuccessState: true, + surfaceExecutionStates: expiryOpiton.item2 == -1 ? false : true, + onTap: () async { + int newValidTill = -1; + final int expireAfterInMicroseconds = expiryOpiton.item2; + // need to manually select time + if (expireAfterInMicroseconds < 0) { + final timeInMicrosecondsFromEpoch = + await _showDateTimePicker(context); + if (timeInMicrosecondsFromEpoch != null) { + newValidTill = timeInMicrosecondsFromEpoch; + } + } else if (expireAfterInMicroseconds == 0) { + // no expiry + newValidTill = 0; + } else { + newValidTill = + DateTime.now().microsecondsSinceEpoch + expireAfterInMicroseconds; + } + if (newValidTill >= 0) { + debugPrint("Setting expirty $newValidTill"); + await updateTime(newValidTill, context); + } + }, + ); + } + + // _showDateTimePicker return null if user doesn't select date-time + Future _showDateTimePicker(BuildContext context) async { + final dateResult = await DatePicker.showDatePicker( + context, + minTime: DateTime.now(), + currentTime: DateTime.now(), + locale: LocaleType.en, + theme: Theme.of(context).colorScheme.dateTimePickertheme, + ); + if (dateResult == null) { + return null; + } + final dateWithTimeResult = await DatePicker.showTime12hPicker( + context, + showTitleActions: true, + currentTime: dateResult, + locale: LocaleType.en, + theme: Theme.of(context).colorScheme.dateTimePickertheme, + ); + if (dateWithTimeResult == null) { + return null; + } else { + return dateWithTimeResult.microsecondsSinceEpoch; + } + } + + Future updateTime(int newValidTill, BuildContext context) async { + await _updateUrlSettings( + context, + {'validTill': newValidTill}, + ); + } + + Future _updateUrlSettings( + BuildContext context, + Map prop, + ) async { + try { + await CollectionsService.instance.updateShareUrl(collection, prop); + } catch (e) { + showGenericErrorDialog(context: context); + rethrow; + } + } +} diff --git a/lib/ui/sharing/share_collection_page.dart b/lib/ui/sharing/share_collection_page.dart index 526fec434..473c0dd6a 100644 --- a/lib/ui/sharing/share_collection_page.dart +++ b/lib/ui/sharing/share_collection_page.dart @@ -8,7 +8,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/divider_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/sharing/add_partipant_page.dart'; @@ -46,7 +46,7 @@ class _ShareCollectionPageState extends State { @override Widget build(BuildContext context) { _sharees = widget.collection.sharees ?? []; - final bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false; + final bool hasUrl = widget.collection.hasLink; final children = []; children.add( MenuSectionTitle( @@ -66,17 +66,44 @@ class _ShareCollectionPageState extends State { children.add( MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: _sharees.isEmpty ? "Add email" : "Add more", + captionedTextWidget: const CaptionedTextWidget( + title: "Add viewer", makeTextBold: true, ), leadingIcon: Icons.add, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, - borderRadius: 4.0, isTopBorderRadiusRemoved: _sharees.isNotEmpty, + isBottomBorderRadiusRemoved: true, onTap: () async { - routeToPage(context, AddParticipantPage(widget.collection)).then( + routeToPage( + context, + AddParticipantPage(widget.collection, true), + ).then( + (value) => { + if (mounted) {setState(() => {})} + }, + ); + }, + ), + ); + children.add( + DividerWidget( + dividerType: DividerType.menu, + bgColor: getEnteColorScheme(context).fillFaint, + ), + ); + children.add( + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Add collaborator", + makeTextBold: true, + ), + leadingIcon: Icons.add, + menuItemColor: getEnteColorScheme(context).fillFaint, + isTopBorderRadiusRemoved: true, + onTap: () async { + routeToPage(context, AddParticipantPage(widget.collection, false)) + .then( (value) => { if (mounted) {setState(() => {})} }, @@ -101,9 +128,7 @@ class _ShareCollectionPageState extends State { height: 24, ), MenuSectionTitle( - title: hasUrl - ? "Public link enabled" - : (_sharees.isEmpty ? "Or share a link" : "Share a link"), + title: hasUrl ? "Public link enabled" : "Share a link", iconData: Icons.public, ), ]); @@ -118,7 +143,6 @@ class _ShareCollectionPageState extends State { leadingIcon: Icons.error_outline, leadingIconColor: getEnteColorScheme(context).warning500, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, onTap: () async {}, isBottomBorderRadiusRemoved: true, ), @@ -138,7 +162,7 @@ class _ShareCollectionPageState extends State { ), leadingIcon: Icons.copy, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, + showOnlyLoadingState: true, onTap: () async { await Clipboard.setData(ClipboardData(text: url)); showShortToast(context, "Link copied to clipboard"); @@ -156,7 +180,6 @@ class _ShareCollectionPageState extends State { ), leadingIcon: Icons.adaptive.share, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, onTap: () async { shareText(url); }, @@ -181,7 +204,6 @@ class _ShareCollectionPageState extends State { leadingIcon: Icons.link, trailingIcon: Icons.navigate_next, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIconIsMuted: true, onTap: () async { routeToPage( @@ -198,34 +220,63 @@ class _ShareCollectionPageState extends State { ], ); } else { - children.add( + children.addAll([ MenuItemWidget( captionedTextWidget: const CaptionedTextWidget( title: "Create public link", + makeTextBold: true, ), leadingIcon: Icons.link, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, + isBottomBorderRadiusRemoved: true, + showOnlyLoadingState: true, onTap: () async { - final bool result = await collectionActions.publicLinkToggle( + final bool result = + await collectionActions.enableUrl(context, widget.collection); + if (result && mounted) { + setState(() => {}); + } + }, + ), + _sharees.isEmpty + ? const MenuSectionDescriptionWidget( + content: "Share with non-ente users", + ) + : const SizedBox.shrink(), + const SizedBox( + height: 24, + ), + const MenuSectionTitle( + title: "Collaborative link", + iconData: Icons.public, + ), + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Collect photos", + makeTextBold: true, + ), + leadingIcon: Icons.link, + menuItemColor: getEnteColorScheme(context).fillFaint, + showOnlyLoadingState: true, + onTap: () async { + final bool result = await collectionActions.enableUrl( context, widget.collection, - true, + enableCollect: true, ); if (result && mounted) { setState(() => {}); } }, ), - ); - if (_sharees.isEmpty && !hasUrl) { - children.add( - const MenuSectionDescriptionWidget( - content: - "Links allow people without an ente account to view and add photos to your shared albums.", - ), - ); - } + _sharees.isEmpty + ? const MenuSectionDescriptionWidget( + content: + "Create a link to allow people to add and view photos in " + "your shared album without needing an ente app or account. Great for collecting event photos.", + ) + : const SizedBox.shrink(), + ]); } return Scaffold( @@ -244,6 +295,7 @@ class _ShareCollectionPageState extends State { padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), @@ -282,7 +334,6 @@ class EmailItemWidget extends StatelessWidget { ), leadingIconSize: 24, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIconIsMuted: true, trailingIcon: Icons.chevron_right, onTap: () async { @@ -308,7 +359,6 @@ class EmailItemWidget extends StatelessWidget { ), leadingIcon: Icons.people_outline, menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: getEnteColorScheme(context).fillFaint, trailingIconIsMuted: true, trailingIcon: Icons.chevron_right, onTap: () async { diff --git a/lib/ui/sharing/user_avator_widget.dart b/lib/ui/sharing/user_avator_widget.dart index 18223183d..873cba05a 100644 --- a/lib/ui/sharing/user_avator_widget.dart +++ b/lib/ui/sharing/user_avator_widget.dart @@ -26,10 +26,13 @@ class UserAvatarWidget extends StatelessWidget { final displayChar = (user.name == null || user.name!.isEmpty) ? ((user.email.isEmpty) ? " " : user.email.substring(0, 1)) : user.name!.substring(0, 1); - final randomColor = colorScheme.avatarColors[ - (user.id ?? 0).remainder(colorScheme.avatarColors.length)]; - final Color decorationColor = - ((user.id ?? -1) == currentUserID) ? Colors.black : randomColor; + Color decorationColor; + if (user.id == null || user.id! <= 0 || user.id == currentUserID) { + decorationColor = Colors.black; + } else { + decorationColor = colorScheme + .avatarColors[(user.id!).remainder(colorScheme.avatarColors.length)]; + } final avatarStyle = getAvatarStyle(context, type); final double size = avatarStyle.item1; diff --git a/lib/ui/tools/debug/app_storage_viewer.dart b/lib/ui/tools/debug/app_storage_viewer.dart index b37b2c84c..7022ff388 100644 --- a/lib/ui/tools/debug/app_storage_viewer.dart +++ b/lib/ui/tools/debug/app_storage_viewer.dart @@ -9,7 +9,7 @@ import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/icon_button_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; @@ -161,9 +161,8 @@ class _AppStorageViewerState extends State { ), menuItemColor: getEnteColorScheme(context).fillFaint, - pressedColor: - getEnteColorScheme(context).fillFaintPressed, - borderRadius: 8, + singleBorderRadius: 8, + alwaysShowSuccessState: true, onTap: () async { for (var pathItem in paths) { if (pathItem.allowCacheClear) { diff --git a/lib/ui/tools/debug/path_storage_viewer.dart b/lib/ui/tools/debug/path_storage_viewer.dart index 37a31bbcb..28de6ad1d 100644 --- a/lib/ui/tools/debug/path_storage_viewer.dart +++ b/lib/ui/tools/debug/path_storage_viewer.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/directory_content.dart'; @@ -76,6 +76,7 @@ class _PathStorageViewerState extends State { Widget _buildMenuItemWidget(DirectoryStat? stat, Object? err) { return MenuItemWidget( + key: UniqueKey(), alignCaptionedTextToLeft: true, captionedTextWidget: CaptionedTextWidget( title: widget.item.title, @@ -98,10 +99,11 @@ class _PathStorageViewerState extends State { ), trailingIcon: err != null ? Icons.error_outline_outlined : null, trailingIconIsMuted: err != null, - borderRadius: 8, + singleBorderRadius: 8, menuItemColor: getEnteColorScheme(context).fillFaint, isBottomBorderRadiusRemoved: widget.removeBottomRadius, isTopBorderRadiusRemoved: widget.removeTopRadius, + showOnlyLoadingState: true, onTap: () async { if (kDebugMode) { await Clipboard.setData(ClipboardData(text: widget.item.path)); diff --git a/lib/ui/viewer/actions/file_selection_actions_widget.dart b/lib/ui/viewer/actions/file_selection_actions_widget.dart index 65363a2c9..2f2c65a48 100644 --- a/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -49,6 +49,7 @@ class _FileSelectionActionWidgetState extends State { late int currentUserID; late FilesSplit split; late CollectionActions collectionActions; + late bool isCollectionOwner; // _cachedCollectionForSharedLink is primarly used to avoid creating duplicate // links if user keeps on creating Create link button after selecting @@ -61,6 +62,8 @@ class _FileSelectionActionWidgetState extends State { split = FilesSplit.split([], currentUserID); widget.selectedFiles.addListener(_selectFileChangeListener); collectionActions = CollectionActions(CollectionsService.instance); + isCollectionOwner = + widget.collection != null && widget.collection!.isOwner(currentUserID); super.initState(); } @@ -88,17 +91,21 @@ class _FileSelectionActionWidgetState extends State { ? " (${split.ownedByCurrentUser.length})" "" : ""; + final int removeCount = split.ownedByCurrentUser.length + + (isCollectionOwner ? split.ownedByOtherUsers.length : 0); + final String removeSuffix = showPrefix + ? " ($removeCount)" + "" + : ""; final String suffixInPending = split.ownedByOtherUsers.isNotEmpty ? " (${split.ownedByCurrentUser.length + split.pendingUploads.length})" "" : ""; + final bool anyOwnedFiles = split.pendingUploads.isNotEmpty || split.ownedByCurrentUser.isNotEmpty; final bool anyUploadedFiles = split.ownedByCurrentUser.isNotEmpty; - bool showRemoveOption = widget.type.showRemoveFromAlbum(); - if (showRemoveOption && widget.type == GalleryType.sharedCollection) { - showRemoveOption = split.ownedByCurrentUser.isNotEmpty; - } + final bool showRemoveOption = widget.type.showRemoveFromAlbum(); debugPrint('$runtimeType building $mounted'); final colorScheme = getEnteColorScheme(context); final List> items = []; @@ -156,9 +163,9 @@ class _FileSelectionActionWidgetState extends State { secondList.add( BlurMenuItemWidget( leadingIcon: Icons.remove_outlined, - labelText: "Remove from album$suffix", + labelText: "Remove from album$removeSuffix", menuItemColor: colorScheme.fillFaint, - onTap: anyUploadedFiles ? _removeFilesFromAlbum : null, + onTap: removeCount > 0 ? _removeFilesFromAlbum : null, ), ); } @@ -233,6 +240,28 @@ class _FileSelectionActionWidgetState extends State { ); } + if (widget.type.showRestoreOption()) { + secondList.add( + BlurMenuItemWidget( + leadingIcon: Icons.visibility, + labelText: "Restore", + menuItemColor: colorScheme.fillFaint, + onTap: _restore, + ), + ); + } + + if (widget.type.showPermanentlyDeleteOption()) { + secondList.add( + BlurMenuItemWidget( + leadingIcon: Icons.delete_forever_outlined, + labelText: "Permanently delete", + menuItemColor: colorScheme.fillFaint, + onTap: _permanentlyDelete, + ), + ); + } + if (firstList.isNotEmpty || secondList.isNotEmpty) { if (firstList.isNotEmpty) { items.add(firstList); @@ -279,16 +308,21 @@ class _FileSelectionActionWidgetState extends State { } Future _removeFilesFromAlbum() async { - if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) { + if (split.pendingUploads.isNotEmpty) { widget.selectedFiles .unSelectAll(split.pendingUploads.toSet(), skipNotify: true); + } + if (!isCollectionOwner && split.ownedByOtherUsers.isNotEmpty) { widget.selectedFiles .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true); } + final bool removingOthersFile = + isCollectionOwner && split.ownedByOtherUsers.isNotEmpty; await collectionActions.showRemoveFromCollectionSheetV2( context, widget.collection!, widget.selectedFiles, + removingOthersFile, ); } @@ -420,4 +454,22 @@ class _FileSelectionActionWidgetState extends State { showShortToast(context, "Link copied to clipboard"); } } + + void _restore() { + createCollectionSheet( + widget.selectedFiles, + null, + context, + actionType: CollectionActionType.restoreFiles, + ); + } + + Future _permanentlyDelete() async { + if (await deleteFromTrash( + context, + widget.selectedFiles.files.toList(), + )) { + widget.selectedFiles.clearAll(); + } + } } diff --git a/lib/ui/viewer/actions/file_selection_overlay_bar.dart b/lib/ui/viewer/actions/file_selection_overlay_bar.dart index 6d20241bb..3b36dad98 100644 --- a/lib/ui/viewer/actions/file_selection_overlay_bar.dart +++ b/lib/ui/viewer/actions/file_selection_overlay_bar.dart @@ -95,11 +95,28 @@ class _FileSelectionOverlayBarState extends State { ), ); } + if (widget.galleryType == GalleryType.trash) { + iconsButton.add( + IconButtonWidget( + icon: Icons.delete_forever_outlined, + iconButtonType: IconButtonType.primary, + iconColor: iconColor, + onTap: () async { + if (await deleteFromTrash( + context, + widget.selectedFiles.files.toList(), + )) { + widget.selectedFiles.clearAll(); + } + }, + ), + ); + } iconsButton.add( IconButtonWidget( icon: Icons.adaptive.share_outlined, iconButtonType: IconButtonType.primary, - iconColor: getEnteColorScheme(context).blurStrokeBase, + iconColor: iconColor, onTap: () => shareSelected( context, shareButtonKey, diff --git a/lib/ui/viewer/file/thumbnail_widget.dart b/lib/ui/viewer/file/thumbnail_widget.dart index 14d24bfc2..e5b787465 100644 --- a/lib/ui/viewer/file/thumbnail_widget.dart +++ b/lib/ui/viewer/file/thumbnail_widget.dart @@ -10,7 +10,6 @@ import 'package:photos/db/files_db.dart'; import 'package:photos/db/trash_db.dart'; import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; -import 'package:photos/extensions/string_ext.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/models/file.dart'; import 'package:photos/models/file_type.dart'; @@ -128,11 +127,10 @@ class _ThumbnailWidgetState extends State { ); } else if (widget.file!.pubMagicMetadata!.uploaderName != null) { contentChildren.add( - // Use uploadName hashCode as userID so that different uploader - // get avatar color + // Use -1 as userID for enforcing black avatar color OwnerAvatarOverlayIcon( User( - id: widget.file!.pubMagicMetadata!.uploaderName.sumAsciiValues, + id: -1, email: '', name: widget.file!.pubMagicMetadata!.uploaderName, ), diff --git a/lib/ui/viewer/gallery/collection_page.dart b/lib/ui/viewer/gallery/collection_page.dart index 62623cebe..cee67ebad 100644 --- a/lib/ui/viewer/gallery/collection_page.dart +++ b/lib/ui/viewer/gallery/collection_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/events/collection_updated_event.dart'; @@ -56,8 +57,8 @@ class _CollectionPageState extends State { if (widget.hasVerifiedLock == false && widget.c.collection.isHidden()) { return const EmptyState(); } - final appBarTypeValue = widget.c.collection.type == CollectionType - .uncategorized ? GalleryType.uncategorized : widget.appBarType; + + final appBarTypeValue = _getGalleryType(widget.c.collection); final List? initialFiles = widget.c.thumbnail != null ? [widget.c.thumbnail!] : null; final gallery = Gallery( @@ -116,6 +117,21 @@ class _CollectionPageState extends State { ); } + GalleryType _getGalleryType(Collection c) { + final currentUserID = Configuration.instance.getUserID()!; + if (!c.isOwner(currentUserID)) { + return GalleryType.sharedCollection; + } + if (c.isDefaultHidden()) { + return GalleryType.hidden; + } else if (c.type == CollectionType.uncategorized) { + return GalleryType.uncategorized; + } else if (c.type == CollectionType.favorites) { + return GalleryType.favorite; + } + return widget.appBarType; + } + _selectedFilesListener() { _selectedFiles.files.isNotEmpty ? _bottomPosition.value = 0.0 diff --git a/lib/ui/viewer/gallery/device_folder_page.dart b/lib/ui/viewer/gallery/device_folder_page.dart index 45c289dd2..2c755a781 100644 --- a/lib/ui/viewer/gallery/device_folder_page.dart +++ b/lib/ui/viewer/gallery/device_folder_page.dart @@ -15,7 +15,7 @@ import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; -import 'package:photos/ui/components/menu_item_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; @@ -30,10 +30,12 @@ class DeviceFolderPage extends StatelessWidget { @override Widget build(Object context) { + final int? userID = Configuration.instance.getUserID(); final gallery = Gallery( asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { return FilesDB.instance.getFilesInDeviceCollection( deviceCollection, + userID, creationStartTime, creationEndTime, limit: limit, @@ -111,7 +113,7 @@ class _BackupHeaderWidgetState extends State { children: [ MenuItemWidget( captionedTextWidget: const CaptionedTextWidget(title: "Backup"), - borderRadius: 8.0, + singleBorderRadius: 8.0, menuItemColor: colorScheme.fillFaint, alignCaptionedTextToLeft: true, trailingWidget: ToggleSwitchWidget( @@ -184,6 +186,7 @@ class _BackupHeaderWidgetState extends State { Future> _filesInDeviceCollection() async { return (await FilesDB.instance.getFilesInDeviceCollection( widget.deviceCollection, + Configuration.instance.getUserID(), galleryLoadStartTime, galleryLoadEndTime, )) @@ -232,9 +235,10 @@ class _ResetIgnoredFilesWidgetState extends State { captionedTextWidget: const CaptionedTextWidget( title: "Reset ignored files", ), - borderRadius: 8.0, + singleBorderRadius: 8.0, menuItemColor: getEnteColorScheme(context).fillFaint, leadingIcon: Icons.cloud_off_outlined, + alwaysShowSuccessState: true, onTap: () async { await _removeFilesFromIgnoredFiles( widget.filesInDeviceCollection, diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 06f947f54..8d723222f 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -18,6 +18,7 @@ import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/common/rename_dialog.dart'; +import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; @@ -138,21 +139,37 @@ class _GalleryAppBarWidgetState extends State { } Future _leaveAlbum(BuildContext context) async { - final result = await showNewChoiceDialog( - context, - title: "Leave shared album", - body: "You will leave the album, and it will stop being visible to you", - firstButtonLabel: "Yes, leave", - isCritical: true, - firstButtonOnTap: () async { - await CollectionsService.instance.leaveAlbum(widget.collection!); - if (mounted) { - Navigator.of(context).pop(); - } - }, + final ButtonAction? result = await showActionSheet( + context: context, + buttons: [ + ButtonWidget( + buttonType: ButtonType.critical, + isInAlert: true, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: true, + labelText: "Leave album", + onTap: () async { + await CollectionsService.instance.leaveAlbum(widget.collection!); + }, + ), + const ButtonWidget( + buttonType: ButtonType.secondary, + buttonAction: ButtonAction.cancel, + isInAlert: true, + shouldStickToDarkTheme: true, + labelText: "Cancel", + ) + ], + title: "Leave shared album?", + body: "Photos added by you will be removed from the album", ); - if (result == ButtonAction.error) { - showGenericErrorDialog(context: context); + if (result != null && mounted) { + if (result == ButtonAction.error) { + showGenericErrorDialog(context: context); + } else if (result == ButtonAction.first) { + Navigator.of(context).pop(); + } } } diff --git a/lib/ui/viewer/gallery/gallery_overlay_widget.dart b/lib/ui/viewer/gallery/gallery_overlay_widget.dart deleted file mode 100644 index a98ea0d0b..000000000 --- a/lib/ui/viewer/gallery/gallery_overlay_widget.dart +++ /dev/null @@ -1,627 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:photos/core/configuration.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/events/subscription_purchased_event.dart'; -import 'package:photos/models/collection.dart'; -import 'package:photos/models/gallery_type.dart'; -import 'package:photos/models/magic_metadata.dart'; -import 'package:photos/models/selected_files.dart'; -import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/hidden_service.dart'; -import 'package:photos/ui/create_collection_sheet.dart'; -import 'package:photos/utils/delete_file_util.dart'; -import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/magic_util.dart'; -import 'package:photos/utils/share_util.dart'; -import 'package:photos/utils/toast_util.dart'; - -class GalleryOverlayWidget extends StatefulWidget { - final GalleryType type; - final SelectedFiles selectedFiles; - final String? path; - final Collection? collection; - - const GalleryOverlayWidget( - this.type, - this.selectedFiles, { - this.path, - this.collection, - Key? key, - }) : super(key: key); - - @override - State createState() => _GalleryOverlayWidgetState(); -} - -class _GalleryOverlayWidgetState extends State { - late StreamSubscription _userAuthEventSubscription; - late Function() _selectedFilesListener; - final GlobalKey shareButtonKey = GlobalKey(); - - @override - void initState() { - _selectedFilesListener = () { - setState(() {}); - }; - widget.selectedFiles.addListener(_selectedFilesListener); - _userAuthEventSubscription = - Bus.instance.on().listen((event) { - setState(() {}); - }); - super.initState(); - } - - @override - void dispose() { - _userAuthEventSubscription.cancel(); - widget.selectedFiles.removeListener(_selectedFilesListener); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty; - final bottomPadding = Platform.isAndroid ? 0.0 : 12.0; - return Padding( - padding: EdgeInsets.only(bottom: bottomPadding), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: filesAreSelected ? 108 : 0, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: filesAreSelected ? 1.0 : 0.0, - curve: Curves.easeIn, - child: IgnorePointer( - ignoring: !filesAreSelected, - child: OverlayWidget( - widget.type, - widget.selectedFiles, - path: widget.path, - collection: widget.collection, - ), - ), - ), - ), - ); - } -} - -class OverlayWidget extends StatefulWidget { - final GalleryType type; - final SelectedFiles selectedFiles; - final String? path; - final Collection? collection; - - const OverlayWidget( - this.type, - this.selectedFiles, { - this.path, - this.collection, - Key? key, - }) : super(key: key); - - @override - State createState() => _OverlayWidgetState(); -} - -class _OverlayWidgetState extends State { - final _logger = Logger("GalleryOverlay"); - late StreamSubscription _userAuthEventSubscription; - late Function() _selectedFilesListener; - final GlobalKey shareButtonKey = GlobalKey(); - - @override - void initState() { - _selectedFilesListener = () { - setState(() {}); - }; - widget.selectedFiles.addListener(_selectedFilesListener); - _userAuthEventSubscription = - Bus.instance.on().listen((event) { - setState(() {}); - }); - super.initState(); - } - - @override - void dispose() { - _userAuthEventSubscription.cancel(); - widget.selectedFiles.removeListener(_selectedFilesListener); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.transparent, - child: ListView( - //ListView is for animation to work without render overflow - physics: const NeverScrollableScrollPhysics(), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), - child: Container( - color: Theme.of(context) - .colorScheme - .frostyBlurBackdropFilterColor, - width: double.infinity, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(13, 13, 0, 13), - child: Text( - widget.selectedFiles.files.length.toString() + - ' selected', - style: Theme.of(context) - .textTheme - .subtitle2! - .copyWith( - fontWeight: FontWeight.w600, - color: - Theme.of(context).colorScheme.iconColor, - ), - ), - ), - Row( - children: _getActions(context), - ) - ], - ), - ), - ), - ), - ), - ), - const Padding(padding: EdgeInsets.symmetric(vertical: 8)), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(24), - child: GestureDetector( - onTap: _clearSelectedFiles, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - //height: 32, - width: 86, - color: Theme.of(context) - .colorScheme - .frostyBlurBackdropFilterColor, - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .subtitle2! - .copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.iconColor, - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - ); - } - - void _clearSelectedFiles() { - widget.selectedFiles.clearAll(); - } - - List _getActions(BuildContext context) { - final List actions = []; - if (widget.type == GalleryType.trash) { - _addTrashAction(actions); - return actions; - } - // skip add button for incoming collection till this feature is implemented - if (Configuration.instance.hasConfiguredAccount() && - widget.type != GalleryType.sharedCollection && - widget.type != GalleryType.hidden) { - IconData iconData = Platform.isAndroid ? Icons.add : CupertinoIcons.add; - // show upload icon instead of add for files selected in local gallery - if (widget.type == GalleryType.localFolder) { - iconData = Icons.cloud_upload_outlined; - } - actions.add( - Tooltip( - message: "add", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: Icon(iconData), - onPressed: () async { - await onActionSelected("add"); - }, - ), - ), - ); - } - - if (Configuration.instance.hasConfiguredAccount() && - widget.type == GalleryType.ownedCollection && - widget.collection!.type != CollectionType.favorites) { - actions.add( - Tooltip( - message: "Move", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: Icon( - Platform.isAndroid - ? Icons.arrow_forward - : CupertinoIcons.arrow_right, - ), - onPressed: () { - onActionSelected('move'); - }, - ), - ), - ); - } - actions.add( - Tooltip( - message: "Share", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - key: shareButtonKey, - icon: Icon(Platform.isAndroid ? Icons.share : CupertinoIcons.share), - onPressed: () { - _shareSelected(context); - }, - ), - ), - ); - if (widget.type == GalleryType.homepage || - widget.type == GalleryType.archive || - widget.type == GalleryType.hidden || - widget.type == GalleryType.localFolder || - widget.type == GalleryType.searchResults) { - actions.add( - Tooltip( - message: "Delete", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: - Icon(Platform.isAndroid ? Icons.delete : CupertinoIcons.delete), - onPressed: () { - _showDeleteSheet(context); - }, - ), - ), - ); - } else if (widget.type == GalleryType.ownedCollection) { - if (widget.collection!.type == CollectionType.folder) { - actions.add( - Tooltip( - message: "Delete", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: Icon( - Platform.isAndroid ? Icons.delete : CupertinoIcons.delete, - ), - onPressed: () { - _showDeleteSheet(context); - }, - ), - ), - ); - } else { - actions.add( - Tooltip( - message: "Remove", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: const Icon( - Icons.remove_circle_rounded, - ), - onPressed: () { - _showRemoveFromCollectionSheet(context); - }, - ), - ), - ); - } - } - - if (widget.type == GalleryType.homepage || - widget.type == GalleryType.archive) { - final bool showArchive = widget.type == GalleryType.homepage; - if (showArchive) { - actions.add( - Tooltip( - message: 'Archive', - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: () { - onActionSelected('archive'); - }, - ), - ), - ); - } else { - actions.insert( - 0, - Tooltip( - message: 'Unarchive', - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: const Icon( - Icons.unarchive, - ), - onPressed: () { - onActionSelected('unarchive'); - }, - ), - ), - ); - } - } - - return actions; - } - - Future onActionSelected(String value) async { - debugPrint("Action Selected $value"); - switch (value.toLowerCase()) { - case 'hide': - await _handleHideRequest(context); - break; - case 'archive': - await _handleVisibilityChangeRequest(context, visibilityArchive); - break; - case 'unarchive': - await _handleVisibilityChangeRequest(context, visibilityVisible); - break; - default: - break; - } - } - - void _addTrashAction(List actions) { - actions.add( - Tooltip( - message: "Restore", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: const Icon( - Icons.restore, - ), - onPressed: () { - createCollectionSheet( - widget.selectedFiles, - null, - context, - actionType: CollectionActionType.restoreFiles, - ); - }, - ), - ), - ); - actions.add( - Tooltip( - message: "Delete permanently", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: const Icon( - Icons.delete_forever, - ), - onPressed: () async { - if (await deleteFromTrash( - context, - widget.selectedFiles.files.toList(), - )) { - _clearSelectedFiles(); - } - }, - ), - ), - ); - } - - Future _handleVisibilityChangeRequest( - BuildContext context, - int newVisibility, - ) async { - try { - await changeVisibility( - context, - widget.selectedFiles.files.toList(), - newVisibility, - ); - } catch (e, s) { - _logger.severe("failed to update file visibility", e, s); - await showGenericErrorDialog(context: context); - } finally { - _clearSelectedFiles(); - } - } - - // note: Keeping this method here so that it can be used whenever we move to - // to bottom UI - Future _handleHideRequest(BuildContext context) async { - try { - final hideResult = await CollectionsService.instance - .hideFiles(context, widget.selectedFiles.files.toList()); - if (hideResult) { - _clearSelectedFiles(); - } - } catch (e, s) { - _logger.severe("failed to update file visibility", e, s); - await showGenericErrorDialog(context: context); - } - } - - void _shareSelected(BuildContext context) { - share( - context, - widget.selectedFiles.files.toList(), - shareButtonKey: shareButtonKey, - ); - } - - void _showDeleteSheet(BuildContext context) { - final count = widget.selectedFiles.files.length; - bool containsUploadedFile = false, containsLocalFile = false; - for (final file in widget.selectedFiles.files) { - if (file.uploadedFileID != null) { - containsUploadedFile = true; - } - if (file.localID != null) { - containsLocalFile = true; - } - } - final actions = []; - if (containsUploadedFile && containsLocalFile) { - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await deleteFilesOnDeviceOnly( - context, - widget.selectedFiles.files.toList(), - ); - _clearSelectedFiles(); - showToast(context, "Files deleted from device"); - }, - child: const Text("Device"), - ), - ); - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await deleteFilesFromRemoteOnly( - context, - widget.selectedFiles.files.toList(), - ); - _clearSelectedFiles(); - showShortToast(context, "Moved to trash"); - }, - child: const Text("ente"), - ), - ); - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await deleteFilesFromEverywhere( - context, - widget.selectedFiles.files.toList(), - ); - _clearSelectedFiles(); - }, - child: const Text("Everywhere"), - ), - ); - } else { - actions.add( - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - await deleteFilesFromEverywhere( - context, - widget.selectedFiles.files.toList(), - ); - _clearSelectedFiles(); - }, - child: const Text("Delete"), - ), - ); - } - final action = CupertinoActionSheet( - title: Text( - "Delete " + - count.toString() + - " file" + - (count == 1 ? "" : "s") + - (containsUploadedFile && containsLocalFile ? " from" : "?"), - ), - actions: actions, - cancelButton: CupertinoActionSheetAction( - child: const Text("Cancel"), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }, - ), - ); - showCupertinoModalPopup( - context: context, - builder: (_) => action, - barrierColor: Colors.black.withOpacity(0.75), - ); - } - - void _showRemoveFromCollectionSheet(BuildContext context) { - final count = widget.selectedFiles.files.length; - final action = CupertinoActionSheet( - title: Text( - "Remove " + - count.toString() + - " file" + - (count == 1 ? "" : "s") + - " from " + - widget.collection!.name! + - "?", - ), - actions: [ - CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop(); - final dialog = createProgressDialog(context, "Removing files..."); - await dialog.show(); - try { - await CollectionsService.instance.removeFromCollection( - widget.collection!.id, - widget.selectedFiles.files.toList(), - ); - await dialog.hide(); - widget.selectedFiles.clearAll(); - } catch (e, s) { - _logger.severe(e, s); - await dialog.hide(); - showGenericErrorDialog(context: context); - } - }, - child: const Text("Remove"), - ), - ], - cancelButton: CupertinoActionSheetAction( - child: const Text("Cancel"), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }, - ), - ); - showCupertinoModalPopup(context: context, builder: (_) => action); - } -} diff --git a/lib/ui/viewer/gallery/photo_grid_size_picker_page.dart b/lib/ui/viewer/gallery/photo_grid_size_picker_page.dart new file mode 100644 index 000000000..a14787d5e --- /dev/null +++ b/lib/ui/viewer/gallery/photo_grid_size_picker_page.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:photos/core/constants.dart'; +import 'package:photos/core/event_bus.dart'; +import 'package:photos/events/force_reload_home_gallery_event.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/captioned_text_widget.dart'; +import 'package:photos/ui/components/divider_widget.dart'; +import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/components/title_bar_widget.dart'; +import 'package:photos/utils/local_settings.dart'; +import 'package:photos/utils/separators_util.dart'; + +class PhotoGridSizePickerPage extends StatelessWidget { + const PhotoGridSizePickerPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + const TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: "Photo grid size", + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: ItemsWidget(), + ), + ], + ), + ); + }, + childCount: 1, + ), + ), + const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), + ], + ), + ); + } +} + +class ItemsWidget extends StatefulWidget { + const ItemsWidget({super.key}); + + @override + State createState() => _ItemsWidgetState(); +} + +class _ItemsWidgetState extends State { + late int currentGridSize; + List items = []; + final List gridSizes = []; + @override + void initState() { + currentGridSize = LocalSettings.instance.getPhotoGridSize(); + for (int gridSize = photoGridSizeMin; + gridSize <= photoGridSizeMax; + gridSize++) { + gridSizes.add(gridSize); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + items.clear(); + for (int girdSize in gridSizes) { + items.add( + _menuItemForPicker(girdSize), + ); + } + items = addSeparators( + items, + DividerWidget( + dividerType: DividerType.menuNoIcon, + bgColor: getEnteColorScheme(context).fillFaint, + ), + ); + return Column( + mainAxisSize: MainAxisSize.min, + children: items, + ); + } + + Widget _menuItemForPicker(int gridSize) { + return MenuItemWidget( + key: ValueKey(gridSize), + menuItemColor: getEnteColorScheme(context).fillFaint, + captionedTextWidget: CaptionedTextWidget( + title: "$gridSize", + ), + trailingIcon: currentGridSize == gridSize ? Icons.check : null, + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: true, + isBottomBorderRadiusRemoved: true, + showOnlyLoadingState: true, + onTap: () async { + await LocalSettings.instance.setPhotoGridSize(gridSize).then( + (value) => setState(() { + currentGridSize = gridSize; + }), + ); + Bus.instance.fire( + ForceReloadHomeGalleryEvent("grid size changed"), + ); + }, + ); + } +} diff --git a/lib/ui/viewer/gallery/trash_page.dart b/lib/ui/viewer/gallery/trash_page.dart index c56431d06..6a60e365f 100644 --- a/lib/ui/viewer/gallery/trash_page.dart +++ b/lib/ui/viewer/gallery/trash_page.dart @@ -9,9 +9,9 @@ import 'package:photos/events/force_reload_trash_page_event.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/ui/common/bottom_shadow.dart'; +import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; -import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; class TrashPage extends StatefulWidget { @@ -109,10 +109,7 @@ class _TrashPageState extends State { ), ), ), - GalleryOverlayWidget( - widget.overlayType, - widget._selectedFiles, - ) + FileSelectionOverlayBar(GalleryType.trash, widget._selectedFiles) ], ), ); @@ -126,7 +123,7 @@ class _TrashPageState extends State { return Padding( padding: const EdgeInsets.all(16), child: Text( - 'Items show the number the days remaining before permanent deletion', + 'Items show the number of days remaining before permanent deletion', style: Theme.of(context).textTheme.caption!.copyWith(fontSize: 16), ), diff --git a/lib/utils/crypto_util.dart b/lib/utils/crypto_util.dart index 36a5d7161..192b4f6e1 100644 --- a/lib/utils/crypto_util.dart +++ b/lib/utils/crypto_util.dart @@ -290,13 +290,17 @@ class CryptoUtil { int memLimit = Sodium.cryptoPwhashMemlimitSensitive; int opsLimit = Sodium.cryptoPwhashOpslimitSensitive; Uint8List key; - while (memLimit > Sodium.cryptoPwhashMemlimitMin && - opsLimit < Sodium.cryptoPwhashOpslimitMax) { + while (memLimit >= Sodium.cryptoPwhashMemlimitMin && + opsLimit <= Sodium.cryptoPwhashOpslimitMax) { try { key = await deriveKey(password, salt, memLimit, opsLimit); return DerivedKeyResult(key, memLimit, opsLimit); } catch (e, s) { - logger.severe(e, s); + logger.severe( + "failed to derive memLimit: $memLimit and opsLimit: $opsLimit", + e, + s, + ); } memLimit = (memLimit / 2).round(); opsLimit = opsLimit * 2; diff --git a/lib/utils/delete_file_util.dart b/lib/utils/delete_file_util.dart index 2c285fc6d..cf86f1aa0 100644 --- a/lib/utils/delete_file_util.dart +++ b/lib/utils/delete_file_util.dart @@ -20,7 +20,6 @@ import 'package:photos/models/trash_item_request.dart'; import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/trash_sync_service.dart'; -import 'package:photos/ui/common/dialogs.dart'; import 'package:photos/ui/common/linear_progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; @@ -35,8 +34,6 @@ Future deleteFilesFromEverywhere( BuildContext context, List files, ) async { - final dialog = createProgressDialog(context, "Deleting..."); - await dialog.show(); _logger.info("Trying to deleteFilesFromEverywhere " + files.toString()); final List localAssetIDs = []; final List localSharedMediaIDs = []; @@ -60,7 +57,6 @@ Future deleteFilesFromEverywhere( if (hasLocalOnlyFiles && Platform.isAndroid) { final shouldProceed = await shouldProceedWithDeletion(context); if (!shouldProceed) { - await dialog.hide(); return; } } @@ -102,12 +98,9 @@ Future deleteFilesFromEverywhere( uploadedFilesToBeTrashed.map((item) => item.fileID).toList(); await TrashSyncService.instance .trashFilesOnServer(uploadedFilesToBeTrashed); - // await SyncService.instance - // .deleteFilesOnServer(fileIDs); await FilesDB.instance.deleteMultipleUploadedFiles(fileIDs); } catch (e) { _logger.severe(e); - await dialog.hide(); showGenericErrorDialog(context: context); rethrow; } @@ -138,7 +131,6 @@ Future deleteFilesFromEverywhere( showShortToast(context, "Moved to trash"); } } - await dialog.hide(); if (uploadedFilesToBeTrashed.isNotEmpty) { RemoteSyncService.instance.sync(silently: true); } @@ -153,8 +145,6 @@ Future deleteFilesFromRemoteOnly( showToast(context, "Selected files are not on ente"); return; } - final dialog = createProgressDialog(context, "Deleting..."); - await dialog.show(); _logger.info( "Trying to deleteFilesFromRemoteOnly " + files.map((f) => f.uploadedFileID).toString(), @@ -172,7 +162,6 @@ Future deleteFilesFromRemoteOnly( await FilesDB.instance.deleteMultipleUploadedFiles(uploadedFileIDs); } catch (e, s) { _logger.severe("Failed to delete files from remote", e, s); - await dialog.hide(); showGenericErrorDialog(context: context); rethrow; } @@ -194,7 +183,6 @@ Future deleteFilesFromRemoteOnly( ), ); SyncService.instance.sync(); - await dialog.hide(); RemoteSyncService.instance.sync(silently: true); } @@ -202,8 +190,6 @@ Future deleteFilesOnDeviceOnly( BuildContext context, List files, ) async { - final dialog = createProgressDialog(context, "Deleting..."); - await dialog.show(); _logger.info("Trying to deleteFilesOnDeviceOnly" + files.toString()); final List localAssetIDs = []; final List localSharedMediaIDs = []; @@ -227,7 +213,6 @@ Future deleteFilesOnDeviceOnly( if (hasLocalOnlyFiles && Platform.isAndroid) { final shouldProceed = await shouldProceedWithDeletion(context); if (!shouldProceed) { - await dialog.hide(); return; } } @@ -258,18 +243,19 @@ Future deleteFilesOnDeviceOnly( ), ); } - await dialog.hide(); } Future deleteFromTrash(BuildContext context, List files) async { - final result = await showNewChoiceDialog( + bool didDeletionStart = false; + final result = await showChoiceActionSheet( context, - title: "Delete permanently", + title: "Permanently delete?", body: "This action cannot be undone", firstButtonLabel: "Delete", isCritical: true, firstButtonOnTap: () async { try { + didDeletionStart = true; await TrashSyncService.instance.deleteFromTrash(files); Bus.instance.fire( FilesUpdatedEvent( @@ -289,16 +275,18 @@ Future deleteFromTrash(BuildContext context, List files) async { return false; } if (result == null || result == ButtonAction.cancel) { - return false; + return didDeletionStart ? true : false; } else { return true; } } Future emptyTrash(BuildContext context) async { - final result = await showNewChoiceDialog( + final result = await showChoiceActionSheet( context, - title: "Empty trash", + title: "Empty trash?", + body: + "All items in trash will be permanently deleted\n\nThis action cannot be undone", firstButtonLabel: "Empty", isCritical: true, firstButtonOnTap: () async { @@ -479,22 +467,25 @@ Future> _tryDeleteSharedMediaFiles(List localIDs) { } Future shouldProceedWithDeletion(BuildContext context) async { - final choice = await showChoiceDialog( + final choice = await showChoiceActionSheet( context, - "Are you sure?", - "Some of the files you are trying to delete are only available on your device and cannot be recovered if deleted", - firstAction: "Cancel", - secondAction: "Delete", - secondActionColor: Colors.red, + title: "Permanently delete from device?", + body: + "Some of the files you are trying to delete are only available on your device and cannot be recovered if deleted", + firstButtonLabel: "Delete", + isCritical: true, ); - return choice == DialogUserChoice.secondChoice; + if (choice == null) { + return false; + } else { + return choice == ButtonAction.first; + } } Future showDeleteSheet( BuildContext context, SelectedFiles selectedFiles, ) async { - final count = selectedFiles.files.length; bool containsUploadedFile = false, containsLocalFile = false; for (final file in selectedFiles.files) { if (file.uploadedFileID != null) { @@ -576,8 +567,6 @@ Future showDeleteSheet( context, selectedFiles.files.toList(), ); - // Navigator.of(context, rootNavigator: true).pop(); - // widget.onFileRemoved(file); }, ), ); diff --git a/lib/utils/dialog_util.dart b/lib/utils/dialog_util.dart index 108d5edd6..d94764de6 100644 --- a/lib/utils/dialog_util.dart +++ b/lib/utils/dialog_util.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/common/progress_dialog.dart'; +import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; @@ -43,7 +44,8 @@ Future showGenericErrorDialog({ context: context, title: "Error", icon: Icons.error_outline_outlined, - body: "It looks like something went wrong. Please try again.", + body: + "It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team.", isDismissible: isDismissible, buttons: const [ ButtonWidget( @@ -90,7 +92,7 @@ DialogWidget choiceDialog({ } ///Will return null if dismissed by tapping outside -Future showNewChoiceDialog( +Future showChoiceDialog( BuildContext context, { required String title, String? body, @@ -132,6 +134,50 @@ Future showNewChoiceDialog( ); } +///Will return null if dismissed by tapping outside +Future showChoiceActionSheet( + BuildContext context, { + required String title, + String? body, + required String firstButtonLabel, + String secondButtonLabel = "Cancel", + ButtonType firstButtonType = ButtonType.neutral, + ButtonType secondButtonType = ButtonType.secondary, + ButtonAction firstButtonAction = ButtonAction.first, + ButtonAction secondButtonAction = ButtonAction.cancel, + FutureVoidCallback? firstButtonOnTap, + FutureVoidCallback? secondButtonOnTap, + bool isCritical = false, + IconData? icon, + bool isDismissible = true, +}) async { + final buttons = [ + ButtonWidget( + buttonType: isCritical ? ButtonType.critical : firstButtonType, + labelText: firstButtonLabel, + isInAlert: true, + onTap: firstButtonOnTap, + buttonAction: firstButtonAction, + shouldStickToDarkTheme: true, + ), + ButtonWidget( + buttonType: secondButtonType, + labelText: secondButtonLabel, + isInAlert: true, + onTap: secondButtonOnTap, + buttonAction: secondButtonAction, + shouldStickToDarkTheme: true, + ), + ]; + return showActionSheet( + context: context, + title: title, + body: body, + buttons: buttons, + isDismissible: isDismissible, + ); +} + ProgressDialog createProgressDialog( BuildContext context, String message, { diff --git a/lib/utils/diff_fetcher.dart b/lib/utils/diff_fetcher.dart index 53afb7a64..f0f7b7396 100644 --- a/lib/utils/diff_fetcher.dart +++ b/lib/utils/diff_fetcher.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/models/file.dart'; import 'package:photos/models/magic_metadata.dart'; @@ -12,7 +12,7 @@ import 'package:photos/utils/file_download_util.dart'; class DiffFetcher { final _logger = Logger("DiffFetcher"); - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; Future getEncryptedFilesDiff(int collectionID, int sinceTime) async { _logger.info( diff --git a/lib/utils/email_util.dart b/lib/utils/email_util.dart index e1264a546..63d1e061f 100644 --- a/lib/utils/email_util.dart +++ b/lib/utils/email_util.dart @@ -253,7 +253,7 @@ Future _clientInfo() async { } void _showNoMailAppsDialog(BuildContext context, String toEmail) { - showNewChoiceDialog( + showChoiceDialog( context, icon: Icons.email_outlined, title: 'Please email us at $toEmail', diff --git a/lib/utils/file_download_util.dart b/lib/utils/file_download_util.dart index 8edb783bb..a5bec8d9a 100644 --- a/lib/utils/file_download_util.dart +++ b/lib/utils/file_download_util.dart @@ -5,7 +5,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/models/file.dart' as ente; import 'package:photos/services/collections_service.dart'; import 'package:photos/utils/crypto_util.dart'; @@ -22,7 +22,7 @@ Future downloadAndDecrypt( ".encrypted"; final encryptedFile = io.File(encryptedFilePath); final startTime = DateTime.now().millisecondsSinceEpoch; - return Network.instance + return NetworkClient.instance .getDio() .download( file.downloadUrl, diff --git a/lib/utils/file_uploader.dart b/lib/utils/file_uploader.dart index d6b56419d..ec1f6ece3 100644 --- a/lib/utils/file_uploader.dart +++ b/lib/utils/file_uploader.dart @@ -15,7 +15,7 @@ import 'package:path/path.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/errors.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/db/upload_locks_db.dart'; import 'package:photos/events/files_updated_event.dart'; @@ -44,8 +44,8 @@ class FileUploader { static const kFileUploadTimeout = Duration(minutes: 50); final _logger = Logger("FileUploader"); - final _dio = Network.instance.getDio(); - final _enteDio = Network.instance.enteDio; + final _dio = NetworkClient.instance.getDio(); + final _enteDio = NetworkClient.instance.enteDio; final LinkedHashMap _queue = LinkedHashMap(); final _uploadLocks = UploadLocksDB.instance; final kSafeBufferForLockExpiry = const Duration(days: 1).inMicroseconds; @@ -603,7 +603,9 @@ class FileUploader { // case c and d final File? fileExistsButDifferentCollection = existingUploadedFiles.firstWhereOrNull( - (e) => e.collectionID != toCollectionID, + (e) => + e.collectionID != toCollectionID && + (e.localID == null || e.localID == fileToUpload.localID), ); if (fileExistsButDifferentCollection != null) { _logger.fine( diff --git a/lib/utils/thumbnail_util.dart b/lib/utils/thumbnail_util.dart index 4dd09f7ee..916c9ff0b 100644 --- a/lib/utils/thumbnail_util.dart +++ b/lib/utils/thumbnail_util.dart @@ -11,7 +11,7 @@ import 'package:photos/core/cache/thumbnail_in_memory_cache.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/models/file.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/file_download_util.dart'; @@ -129,7 +129,7 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { final file = item.file; Uint8List encryptedThumbnail; try { - encryptedThumbnail = (await Network.instance.getDio().get( + encryptedThumbnail = (await NetworkClient.instance.getDio().get( file.thumbnailUrl, options: Options( headers: {"X-Auth-Token": Configuration.instance.getToken()}, diff --git a/lib/utils/trash_diff_fetcher.dart b/lib/utils/trash_diff_fetcher.dart index a9f8aab8f..5283e5464 100644 --- a/lib/utils/trash_diff_fetcher.dart +++ b/lib/utils/trash_diff_fetcher.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; -import 'package:photos/core/network.dart'; +import 'package:photos/core/network/network.dart'; import 'package:photos/models/magic_metadata.dart'; import 'package:photos/models/trash_file.dart'; import 'package:photos/utils/crypto_util.dart'; @@ -11,7 +11,7 @@ import 'package:photos/utils/file_download_util.dart'; class TrashDiffFetcher { final _logger = Logger("TrashDiffFetcher"); - final _enteDio = Network.instance.enteDio; + final _enteDio = NetworkClient.instance.enteDio; Future getTrashFilesDiff(int sinceTime) async { try { diff --git a/pubspec.yaml b/pubspec.yaml index c4437297b..5002de450 100644 --- a/pubspec.yaml +++ b/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.7.8+408 +version: 0.7.20+420 environment: sdk: '>=2.17.0 <3.0.0'