Merge branch 'main' into flutter_3.7.1
This commit is contained in:
commit
a56d44c45c
86 changed files with 1769 additions and 1980 deletions
11
fastlane/metadata/android/en-US/changelogs/420.txt
Normal file
11
fastlane/metadata/android/en-US/changelogs/420.txt
Normal file
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -55,3 +55,5 @@ const int intMaxValue = 9223372036854775807;
|
|||
const double restrictedMaxWidth = 430;
|
||||
|
||||
const double mobileSmallThreshold = 336;
|
||||
|
||||
const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];
|
||||
|
|
29
lib/core/network/ente_interceptor.dart
Normal file
29
lib/core/network/ente_interceptor.dart
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -307,6 +307,7 @@ extension DeviceFiles on FilesDB {
|
|||
|
||||
Future<FileLoadResult> 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
|
||||
|
|
|
@ -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.",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -763,13 +763,15 @@ class FilesDB {
|
|||
return convertToFiles(results)[0];
|
||||
}
|
||||
|
||||
Future<Set<String>> getExistingLocalFileIDs() async {
|
||||
Future<Set<String>> 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 = <String>{};
|
||||
for (final row in rows) {
|
||||
|
|
|
@ -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<void> _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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = <String, int>{};
|
||||
final _collectionIDToCollections = <int, Collection>{};
|
||||
final _cachedKeys = <int, Uint8List>{};
|
||||
|
@ -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<void> createShareUrl(Collection collection) async {
|
||||
Future<void> 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 = <String, dynamic>{};
|
||||
params["collectionID"] = toCollectionID;
|
||||
final toCollectionKey = getCollectionKey(toCollectionID);
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
final Set<String> 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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<void> 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);
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<void>();
|
||||
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<bool> 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<String, Set<String>> pathToLocalIDs =
|
||||
await _db.getDevicePathIDToLocalIDMap();
|
||||
final invalidIDs = _getInvalidFileIDs().toSet();
|
||||
|
@ -236,15 +236,6 @@ class LocalSyncService {
|
|||
return hasUnsyncedFiles;
|
||||
}
|
||||
|
||||
List<String> _getEditedFileIDs() {
|
||||
if (_prefs.containsKey(kEditedFileIDsKey)) {
|
||||
return _prefs.getStringList(kEditedFileIDsKey)!;
|
||||
} else {
|
||||
final List<String> editedIDs = [];
|
||||
return editedIDs;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> _loadAndStorePhotos(
|
||||
int fromTime,
|
||||
int toTime,
|
||||
Set<String> existingLocalFileIDs,
|
||||
Set<String> editedFileIDs,
|
||||
) async {
|
||||
Future<void> _loadAndStoreDiff(
|
||||
Set<String> existingLocalDs, {
|
||||
required int fromTime,
|
||||
required int toTime,
|
||||
}) async {
|
||||
final Tuple2<List<LocalPathAsset>, List<File>> 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<File> 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<File> 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<void> _trackUpdatedFiles(
|
||||
List<File> files,
|
||||
Set<String> existingLocalFileIDs,
|
||||
Set<String> editedFileIDs,
|
||||
) async {
|
||||
final updatedFiles = files
|
||||
.where((file) => existingLocalFileIDs.contains(file.localID))
|
||||
final List<String> 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<String> updatedLocalIDs = [];
|
||||
for (final file in updatedFiles) {
|
||||
if (file.localID != null) {
|
||||
updatedLocalIDs.add(file.localID!);
|
||||
}
|
||||
}
|
||||
if (updatedLocalIDs.isNotEmpty) {
|
||||
await FileUpdationDB.instance.insertMultiple(
|
||||
updatedLocalIDs,
|
||||
FileUpdationDB.modificationTimeUpdated,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<List<File>>? _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(),
|
||||
)) {
|
||||
|
|
|
@ -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<bool>? _existingSync;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<LatestVersionInfo> _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"]);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<String?> 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<void> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -80,7 +80,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
} 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<PasswordReentryPage> {
|
|||
} 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",
|
||||
|
|
|
@ -22,7 +22,9 @@ class _SessionsPageState extends State<SessionsPage> {
|
|||
|
||||
@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<SessionsPage> {
|
|||
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<SessionsPage> {
|
|||
}
|
||||
|
||||
Future<void> _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(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
|
|||
"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,
|
||||
|
|
|
@ -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<void> 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: <Widget>[
|
||||
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<bool> _anyItemPresentOnlyInCurrentAlbum(
|
||||
Set<File> files,
|
||||
int collectionID,
|
||||
) async {
|
||||
final List<int> uploadedIDs = files
|
||||
.where((e) => e.uploadedFileID != null)
|
||||
.map((e) => e.uploadedFileID!)
|
||||
.toList();
|
||||
|
||||
final Map<int, List<File>> collectionToFilesMap =
|
||||
await FilesDB.instance.getAllFilesGroupByCollectionID(uploadedIDs);
|
||||
final Set<int> ids = uploadedIDs.toSet();
|
||||
for (MapEntry<int, List<File>> 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<bool> updateFavorites(
|
||||
BuildContext context,
|
||||
List<File> files,
|
||||
|
|
|
@ -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<bool> publicLinkToggle(
|
||||
Future<bool> 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<bool> 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<Collection?> createSharedCollectionLink(
|
||||
BuildContext context,
|
||||
List<File> files,
|
||||
|
@ -137,44 +135,55 @@ class CollectionActions {
|
|||
}
|
||||
|
||||
// removeParticipant remove the user from a share album
|
||||
Future<bool?> removeParticipant(
|
||||
Future<bool> 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<bool?> addEmailToCollection(
|
||||
// addEmailToCollection returns true if add operation was successful
|
||||
Future<bool> 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<bool> 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<bool> _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;
|
||||
}
|
||||
|
|
|
@ -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<AdvancedSettingsScreen> {
|
||||
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<AdvancedSettingsScreen> {
|
|||
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<AdvancedSettingsScreen> {
|
|||
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<AdvancedSettingsScreen> {
|
|||
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<AdvancedSettingsScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showPhotoGridSizePicker(BuildContext buildContext) async {
|
||||
final textTheme = getEnteTextTheme(buildContext);
|
||||
final List<Text> 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: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: getEnteColorScheme(buildContext).backgroundElevated2,
|
||||
border: const Border(
|
||||
bottom: BorderSide(
|
||||
color: Color(0xff999999),
|
||||
width: 0.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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<DialogUserChoice?> showChoiceDialog<T>(
|
||||
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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -37,6 +37,7 @@ class AlbumListItemWidget extends StatelessWidget {
|
|||
? ThumbnailWidget(
|
||||
item.thumbnail,
|
||||
showFavForAlbumOnly: true,
|
||||
shouldShowOwnerAvatar: false,
|
||||
)
|
||||
: const NoThumbnailWidget(
|
||||
addBorder: false,
|
||||
|
|
|
@ -112,7 +112,7 @@ class ContentContainer extends StatelessWidget {
|
|||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 48,
|
||||
size: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
173
lib/ui/components/menu_item_widget/menu_item_child_widgets.dart
Normal file
173
lib/ui/components/menu_item_widget/menu_item_child_widgets.dart
Normal file
|
@ -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<TrailingWidget> createState() => _TrailingWidgetState();
|
||||
}
|
||||
|
||||
class _TrailingWidgetState extends State<TrailingWidget> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<void> 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<MenuItemWidget> {
|
||||
final _debouncer = Debouncer(const Duration(milliseconds: 300));
|
||||
ValueNotifier<ExecutionState> 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<MenuItemWidget> {
|
|||
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<MenuItemWidget> {
|
|||
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<MenuItemWidget> {
|
|||
}
|
||||
|
||||
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<MenuItemWidget> {
|
|||
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<void> _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<MenuItemWidget> {
|
|||
}
|
||||
|
||||
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<MenuItemWidget> {
|
|||
}
|
||||
|
||||
void _onCancel() {
|
||||
if (executionStateNotifier.value == ExecutionState.inProgress ||
|
||||
executionStateNotifier.value == ExecutionState.successful) return;
|
||||
setState(() {
|
||||
menuItemColor = widget.menuItemColor;
|
||||
});
|
|
@ -9,12 +9,12 @@ enum ExecutionState {
|
|||
successful,
|
||||
}
|
||||
|
||||
typedef FutureVoidCallBack = Future<void> Function();
|
||||
typedef FutureVoidCallback = Future<void> 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,
|
||||
|
|
|
@ -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<CreateCollectionSheet> {
|
|||
}
|
||||
|
||||
Future<bool> _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<File> files = [];
|
||||
final List<File> filesPendingUpload = [];
|
||||
final int currentUserID = Configuration.instance.getUserID()!;
|
||||
if (widget.sharedFiles != null) {
|
||||
filesPendingUpload.addAll(
|
||||
await convertIncomingSharedMediaToFile(
|
||||
|
@ -374,8 +381,17 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
|
|||
);
|
||||
} 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<CreateCollectionSheet> {
|
|||
}
|
||||
}
|
||||
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<CreateCollectionSheet> {
|
|||
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<CreateCollectionSheet> {
|
|||
}
|
||||
|
||||
Future<bool> _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
|
||||
|
|
|
@ -103,49 +103,34 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
|
|||
final List<ChangeLogEntry> 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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -118,7 +118,7 @@ class ChildSubscriptionWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
Future<void> _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?",
|
||||
|
|
|
@ -344,7 +344,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
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<StripeSubscriptionPage> {
|
|||
);
|
||||
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<StripeSubscriptionPage> {
|
|||
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?",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<void> _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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<ApkDownloaderDialog> {
|
|||
|
||||
Future<void> _downloadApk() async {
|
||||
try {
|
||||
await Network.instance.getDio().download(
|
||||
await NetworkClient.instance.getDio().download(
|
||||
widget.versionInfo!.url,
|
||||
_saveUrl,
|
||||
onReceiveProgress: (count, _) {
|
||||
|
|
|
@ -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<BackupSectionWidget> {
|
|||
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<BackupSectionWidget> {
|
|||
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<BackupSectionWidget> {
|
|||
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<BackupSectionWidget> {
|
|||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
showOnlyLoadingState: true,
|
||||
onTap: () async {
|
||||
final dialog = createProgressDialog(context, "Calculating...");
|
||||
await dialog.show();
|
||||
List<DuplicateFiles> duplicates;
|
||||
try {
|
||||
duplicates =
|
||||
await DeduplicationService.instance.getDuplicateFiles();
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
showGenericErrorDialog(context: context);
|
||||
return;
|
||||
}
|
||||
|
||||
await dialog.hide();
|
||||
if (duplicates.isEmpty) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<void> _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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<SecuritySectionWidget> {
|
|||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
showOnlyLoadingState: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<SharedCollectionGallery>
|
|||
final List<CollectionWithThumbnail> outgoing = [];
|
||||
final List<CollectionWithThumbnail> 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<SharedCollectionGallery>
|
|||
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 = <String?>[];
|
||||
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 = <String>[];
|
||||
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,
|
||||
|
|
|
@ -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<StatefulWidget> createState() => _AddParticipantPage();
|
||||
}
|
||||
|
||||
class _AddParticipantPage extends State<AddParticipantPage> {
|
||||
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<AddParticipantPage> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
selectAsViewer = true;
|
||||
collectionActions = CollectionActions(CollectionsService.instance);
|
||||
super.initState();
|
||||
}
|
||||
|
@ -54,12 +52,13 @@ class _AddParticipantPage extends State<AddParticipantPage> {
|
|||
Widget build(BuildContext context) {
|
||||
isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
|
||||
final enteTextTheme = getEnteTextTheme(context);
|
||||
final enteColorScheme = getEnteColorScheme(context);
|
||||
final List<User> 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<AddParticipantPage> {
|
|||
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<AddParticipantPage> {
|
|||
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<AddParticipantPage> {
|
|||
],
|
||||
);
|
||||
},
|
||||
itemCount: suggestedUsers.length,
|
||||
|
||||
itemCount: suggestedUsers.length +
|
||||
(widget.isAddingViewer ? 0 : 1),
|
||||
// physics: const ClampingScrollPhysics(),
|
||||
),
|
||||
),
|
||||
|
@ -146,9 +158,6 @@ class _AddParticipantPage extends State<AddParticipantPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const DividerWidget(
|
||||
dividerType: DividerType.solid,
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
@ -160,74 +169,34 @@ class _AddParticipantPage extends State<AddParticipantPage> {
|
|||
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<AddParticipantPage> {
|
|||
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<AddParticipantPage> {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AlbumParticipantsPage> {
|
|||
Future<void> _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<AlbumParticipantsPage> {
|
|||
final splitResult =
|
||||
widget.collection.getSharees().splitMatch((x) => x.isViewer);
|
||||
final List<User> viewers = splitResult.matched;
|
||||
viewers.sort((a, b) => a.email.compareTo(b.email));
|
||||
final List<User> collaborators = splitResult.unmatched;
|
||||
collaborators.sort((a, b) => a.email.compareTo(b.email));
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
|
@ -110,7 +112,7 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
|
|||
),
|
||||
leadingIconSize: 24,
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
borderRadius: 8,
|
||||
singleBorderRadius: 8,
|
||||
isGestureDetectorDisabled: true,
|
||||
),
|
||||
],
|
||||
|
@ -153,17 +155,18 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
|
|||
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<AlbumParticipantsPage> {
|
|||
} 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<AlbumParticipantsPage> {
|
|||
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<AlbumParticipantsPage> {
|
|||
} 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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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<ManageSharedLinkWidget> {
|
||||
// index, title, milliseconds in future post which link should expire (when >0)
|
||||
final List<Tuple3<int, String, int>> _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<int, String, int> _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<ManageSharedLinkWidget> {
|
|||
),
|
||||
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<ManageSharedLinkWidget> {
|
|||
),
|
||||
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<ManageSharedLinkWidget> {
|
|||
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<ManageSharedLinkWidget> {
|
|||
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<ManageSharedLinkWidget> {
|
|||
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<ManageSharedLinkWidget> {
|
|||
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<ManageSharedLinkWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> showPicker() async {
|
||||
return showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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<void> 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<int?> _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<String?> _displayLinkPasswordInput(BuildContext context) async {
|
||||
|
@ -497,87 +342,4 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
|||
await showGenericErrorDialog(context: context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showDeviceLimitPicker() async {
|
||||
final List<Text> 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: <Widget>[
|
||||
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: <Widget>[
|
||||
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,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
149
lib/ui/sharing/pickers/device_limit_picker_page.dart
Normal file
149
lib/ui/sharing/pickers/device_limit_picker_page.dart
Normal file
|
@ -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: <Widget>[
|
||||
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<ItemsWidget> createState() => _ItemsWidgetState();
|
||||
}
|
||||
|
||||
class _ItemsWidgetState extends State<ItemsWidget> {
|
||||
late int currentDeviceLimit;
|
||||
late int initialDeviceLimit;
|
||||
List<Widget> 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<void> _updateUrlSettings(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> prop,
|
||||
) async {
|
||||
try {
|
||||
await CollectionsService.instance.updateShareUrl(widget.collection, prop);
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context: context);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
181
lib/ui/sharing/pickers/link_expiry_picker_page.dart
Normal file
181
lib/ui/sharing/pickers/link_expiry_picker_page.dart
Normal file
|
@ -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: <Widget>[
|
||||
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<Tuple2<String, int>> _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<Widget> items = [];
|
||||
for (Tuple2<String, int> 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<String, int> 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<int?> _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<void> updateTime(int newValidTill, BuildContext context) async {
|
||||
await _updateUrlSettings(
|
||||
context,
|
||||
{'validTill': newValidTill},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateUrlSettings(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> prop,
|
||||
) async {
|
||||
try {
|
||||
await CollectionsService.instance.updateShareUrl(collection, prop);
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context: context);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ShareCollectionPage> {
|
|||
@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 = <Widget>[];
|
||||
children.add(
|
||||
MenuSectionTitle(
|
||||
|
@ -66,17 +66,44 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
|
|||
|
||||
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<ShareCollectionPage> {
|
|||
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<ShareCollectionPage> {
|
|||
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<ShareCollectionPage> {
|
|||
),
|
||||
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<ShareCollectionPage> {
|
|||
),
|
||||
leadingIcon: Icons.adaptive.share,
|
||||
menuItemColor: getEnteColorScheme(context).fillFaint,
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
onTap: () async {
|
||||
shareText(url);
|
||||
},
|
||||
|
@ -181,7 +204,6 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
|
|||
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<ShareCollectionPage> {
|
|||
],
|
||||
);
|
||||
} 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<ShareCollectionPage> {
|
|||
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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<AppStorageViewer> {
|
|||
),
|
||||
menuItemColor:
|
||||
getEnteColorScheme(context).fillFaint,
|
||||
pressedColor:
|
||||
getEnteColorScheme(context).fillFaintPressed,
|
||||
borderRadius: 8,
|
||||
singleBorderRadius: 8,
|
||||
alwaysShowSuccessState: true,
|
||||
onTap: () async {
|
||||
for (var pathItem in paths) {
|
||||
if (pathItem.allowCacheClear) {
|
||||
|
|
|
@ -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<PathStorageViewer> {
|
|||
|
||||
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<PathStorageViewer> {
|
|||
),
|
||||
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));
|
||||
|
|
|
@ -49,6 +49,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
|
|||
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<FileSelectionActionWidget> {
|
|||
split = FilesSplit.split(<File>[], 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<FileSelectionActionWidget> {
|
|||
? " (${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<List<BlurMenuItemWidget>> items = [];
|
||||
|
@ -156,9 +163,9 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
|
|||
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<FileSelectionActionWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
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<FileSelectionActionWidget> {
|
|||
}
|
||||
|
||||
Future<void> _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<FileSelectionActionWidget> {
|
|||
showShortToast(context, "Link copied to clipboard");
|
||||
}
|
||||
}
|
||||
|
||||
void _restore() {
|
||||
createCollectionSheet(
|
||||
widget.selectedFiles,
|
||||
null,
|
||||
context,
|
||||
actionType: CollectionActionType.restoreFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _permanentlyDelete() async {
|
||||
if (await deleteFromTrash(
|
||||
context,
|
||||
widget.selectedFiles.files.toList(),
|
||||
)) {
|
||||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,11 +95,28 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
|
|||
),
|
||||
);
|
||||
}
|
||||
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,
|
||||
|
|
|
@ -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<ThumbnailWidget> {
|
|||
);
|
||||
} 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,
|
||||
),
|
||||
|
|
|
@ -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<CollectionPage> {
|
|||
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<File>? initialFiles =
|
||||
widget.c.thumbnail != null ? [widget.c.thumbnail!] : null;
|
||||
final gallery = Gallery(
|
||||
|
@ -116,6 +117,21 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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<BackupHeaderWidget> {
|
|||
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<BackupHeaderWidget> {
|
|||
Future<List<File>> _filesInDeviceCollection() async {
|
||||
return (await FilesDB.instance.getFilesInDeviceCollection(
|
||||
widget.deviceCollection,
|
||||
Configuration.instance.getUserID(),
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
))
|
||||
|
@ -232,9 +235,10 @@ class _ResetIgnoredFilesWidgetState extends State<ResetIgnoredFilesWidget> {
|
|||
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,
|
||||
|
|
|
@ -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<GalleryAppBarWidget> {
|
|||
}
|
||||
|
||||
Future<dynamic> _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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<GalleryOverlayWidget> createState() => _GalleryOverlayWidgetState();
|
||||
}
|
||||
|
||||
class _GalleryOverlayWidgetState extends State<GalleryOverlayWidget> {
|
||||
late StreamSubscription _userAuthEventSubscription;
|
||||
late Function() _selectedFilesListener;
|
||||
final GlobalKey shareButtonKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_selectedFilesListener = () {
|
||||
setState(() {});
|
||||
};
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
_userAuthEventSubscription =
|
||||
Bus.instance.on<SubscriptionPurchasedEvent>().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<OverlayWidget> createState() => _OverlayWidgetState();
|
||||
}
|
||||
|
||||
class _OverlayWidgetState extends State<OverlayWidget> {
|
||||
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<SubscriptionPurchasedEvent>().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<Widget> _getActions(BuildContext context) {
|
||||
final List<Widget> actions = <Widget>[];
|
||||
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<void> 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<Widget> 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<void> _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<void> _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 = <Widget>[];
|
||||
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: <Widget>[
|
||||
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);
|
||||
}
|
||||
}
|
124
lib/ui/viewer/gallery/photo_grid_size_picker_page.dart
Normal file
124
lib/ui/viewer/gallery/photo_grid_size_picker_page.dart
Normal file
|
@ -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: <Widget>[
|
||||
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<ItemsWidget> createState() => _ItemsWidgetState();
|
||||
}
|
||||
|
||||
class _ItemsWidgetState extends State<ItemsWidget> {
|
||||
late int currentGridSize;
|
||||
List<Widget> items = [];
|
||||
final List<int> 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"),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<TrashPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
GalleryOverlayWidget(
|
||||
widget.overlayType,
|
||||
widget._selectedFiles,
|
||||
)
|
||||
FileSelectionOverlayBar(GalleryType.trash, widget._selectedFiles)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -126,7 +123,7 @@ class _TrashPageState extends State<TrashPage> {
|
|||
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),
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<void> deleteFilesFromEverywhere(
|
|||
BuildContext context,
|
||||
List<File> files,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Deleting...");
|
||||
await dialog.show();
|
||||
_logger.info("Trying to deleteFilesFromEverywhere " + files.toString());
|
||||
final List<String> localAssetIDs = [];
|
||||
final List<String> localSharedMediaIDs = [];
|
||||
|
@ -60,7 +57,6 @@ Future<void> deleteFilesFromEverywhere(
|
|||
if (hasLocalOnlyFiles && Platform.isAndroid) {
|
||||
final shouldProceed = await shouldProceedWithDeletion(context);
|
||||
if (!shouldProceed) {
|
||||
await dialog.hide();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -102,12 +98,9 @@ Future<void> 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<void> deleteFilesFromEverywhere(
|
|||
showShortToast(context, "Moved to trash");
|
||||
}
|
||||
}
|
||||
await dialog.hide();
|
||||
if (uploadedFilesToBeTrashed.isNotEmpty) {
|
||||
RemoteSyncService.instance.sync(silently: true);
|
||||
}
|
||||
|
@ -153,8 +145,6 @@ Future<void> 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<void> 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<void> deleteFilesFromRemoteOnly(
|
|||
),
|
||||
);
|
||||
SyncService.instance.sync();
|
||||
await dialog.hide();
|
||||
RemoteSyncService.instance.sync(silently: true);
|
||||
}
|
||||
|
||||
|
@ -202,8 +190,6 @@ Future<void> deleteFilesOnDeviceOnly(
|
|||
BuildContext context,
|
||||
List<File> files,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Deleting...");
|
||||
await dialog.show();
|
||||
_logger.info("Trying to deleteFilesOnDeviceOnly" + files.toString());
|
||||
final List<String> localAssetIDs = [];
|
||||
final List<String> localSharedMediaIDs = [];
|
||||
|
@ -227,7 +213,6 @@ Future<void> deleteFilesOnDeviceOnly(
|
|||
if (hasLocalOnlyFiles && Platform.isAndroid) {
|
||||
final shouldProceed = await shouldProceedWithDeletion(context);
|
||||
if (!shouldProceed) {
|
||||
await dialog.hide();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -258,18 +243,19 @@ Future<void> deleteFilesOnDeviceOnly(
|
|||
),
|
||||
);
|
||||
}
|
||||
await dialog.hide();
|
||||
}
|
||||
|
||||
Future<bool> deleteFromTrash(BuildContext context, List<File> 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<bool> deleteFromTrash(BuildContext context, List<File> files) async {
|
|||
return false;
|
||||
}
|
||||
if (result == null || result == ButtonAction.cancel) {
|
||||
return false;
|
||||
return didDeletionStart ? true : false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> 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<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
|
|||
}
|
||||
|
||||
Future<bool> 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<void> 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<void> showDeleteSheet(
|
|||
context,
|
||||
selectedFiles.files.toList(),
|
||||
);
|
||||
// Navigator.of(context, rootNavigator: true).pop();
|
||||
// widget.onFileRemoved(file);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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<ButtonAction?> 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<ButtonAction?> showNewChoiceDialog(
|
||||
Future<ButtonAction?> showChoiceDialog(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
String? body,
|
||||
|
@ -132,6 +134,50 @@ Future<ButtonAction?> showNewChoiceDialog(
|
|||
);
|
||||
}
|
||||
|
||||
///Will return null if dismissed by tapping outside
|
||||
Future<ButtonAction?> 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, {
|
||||
|
|
|
@ -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<Diff> getEncryptedFilesDiff(int collectionID, int sinceTime) async {
|
||||
_logger.info(
|
||||
|
|
|
@ -253,7 +253,7 @@ Future<String> _clientInfo() async {
|
|||
}
|
||||
|
||||
void _showNoMailAppsDialog(BuildContext context, String toEmail) {
|
||||
showNewChoiceDialog(
|
||||
showChoiceDialog(
|
||||
context,
|
||||
icon: Icons.email_outlined,
|
||||
title: 'Please email us at $toEmail',
|
||||
|
|
|
@ -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<io.File?> downloadAndDecrypt(
|
|||
".encrypted";
|
||||
final encryptedFile = io.File(encryptedFilePath);
|
||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
return Network.instance
|
||||
return NetworkClient.instance
|
||||
.getDio()
|
||||
.download(
|
||||
file.downloadUrl,
|
||||
|
|
|
@ -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<String, FileUploadItem>();
|
||||
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(
|
||||
|
|
|
@ -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<void> _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()},
|
||||
|
|
|
@ -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<Diff> getTrashFilesDiff(int sinceTime) async {
|
||||
try {
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Reference in a new issue