Merge branch 'main' into flutter_3.7.1

This commit is contained in:
vishnukvmd 2023-02-08 15:59:49 +05:30
commit a56d44c45c
86 changed files with 1769 additions and 1980 deletions

View 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

View file

@ -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.

View file

@ -55,3 +55,5 @@ const int intMaxValue = 9223372036854775807;
const double restrictedMaxWidth = 430;
const double mobileSmallThreshold = 336;
const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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.",
);
}

View file

@ -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) {

View file

@ -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();

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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,
);

View file

@ -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();

View file

@ -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);

View file

@ -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 =

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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(),
)) {

View file

@ -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;

View file

@ -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;

View file

@ -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"]);

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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:

View file

@ -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",

View file

@ -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(() {});
}
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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,
),

View file

@ -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,
);
}

View file

@ -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,

View file

@ -37,6 +37,7 @@ class AlbumListItemWidget extends StatelessWidget {
? ThumbnailWidget(
item.thumbnail,
showFavForAlbumOnly: true,
shouldShowOwnerAvatar: false,
)
: const NoThumbnailWidget(
addBorder: false,

View file

@ -112,7 +112,7 @@ class ContentContainer extends StatelessWidget {
children: [
Icon(
icon,
size: 48,
size: 32,
),
],
),

View file

@ -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';

View 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,
),
),
),
);
}
}

View file

@ -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;
});

View file

@ -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,

View file

@ -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

View file

@ -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,
),
);

View file

@ -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) {

View file

@ -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?",

View file

@ -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?",

View file

@ -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) {

View file

@ -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);
},
);
}

View file

@ -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, _) {

View file

@ -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,

View file

@ -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';

View file

@ -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);
}

View file

@ -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(

View file

@ -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);
},
);

View file

@ -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) {

View file

@ -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 {

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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);
}
},

View file

@ -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,
),
)
],
);
},
);
}
}

View 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;
}
}
}

View 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;
}
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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) {

View file

@ -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));

View file

@ -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();
}
}
}

View file

@ -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,

View file

@ -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,
),

View file

@ -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

View file

@ -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,

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View 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"),
);
},
);
}
}

View file

@ -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),
),

View file

@ -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;

View file

@ -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);
},
),
);

View 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, {

View file

@ -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(

View file

@ -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',

View file

@ -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,

View file

@ -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(

View file

@ -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()},

View file

@ -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 {

View file

@ -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'