feat: init smart albums concept
This commit is contained in:
@@ -15,6 +15,7 @@ import "package:photos/events/memories_changed_event.dart";
|
||||
import "package:photos/events/people_changed_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/collection/smart_album_config.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/home_widget_service.dart";
|
||||
@@ -74,8 +75,10 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
||||
_peopleChangedSubscription = Bus.instance.on<PeopleChangedEvent>().listen(
|
||||
(event) async {
|
||||
_changeCallbackDebouncer.run(
|
||||
() async =>
|
||||
unawaited(PeopleHomeWidgetService.instance.checkPeopleChanged()),
|
||||
() async {
|
||||
unawaited(PeopleHomeWidgetService.instance.checkPeopleChanged());
|
||||
unawaited(syncSmartAlbums());
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
165
mobile/apps/photos/lib/models/collection/smart_album_config.dart
Normal file
165
mobile/apps/photos/lib/models/collection/smart_album_config.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/service_locator.dart" show entityService, prefs;
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
import "package:photos/ui/actions/collection/collection_file_actions.dart";
|
||||
import "package:photos/ui/actions/collection/collection_sharing_actions.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
import "package:synchronized/synchronized.dart";
|
||||
|
||||
class SmartAlbumConfig {
|
||||
final int collectionId;
|
||||
// person ids
|
||||
final Set<String> personIDs;
|
||||
// person id mapped with updatedat, file ids
|
||||
final Map<String, (int, Set<int>)> addedFiles;
|
||||
|
||||
SmartAlbumConfig({
|
||||
required this.collectionId,
|
||||
required this.personIDs,
|
||||
required this.addedFiles,
|
||||
});
|
||||
|
||||
static const _personIdsKey = "smart_album_person_ids";
|
||||
static const _addedFilesKey = "smart_album_added_files";
|
||||
|
||||
Future<SmartAlbumConfig> getUpdatedConfig(Set<String> newPersonsIds) async {
|
||||
final toAdd = newPersonsIds.difference(personIDs);
|
||||
final toRemove = personIDs.difference(newPersonsIds);
|
||||
final newFiles = Map<String, (int, Set<int>)>.from(addedFiles);
|
||||
|
||||
// Remove whats not needed
|
||||
for (final personId in toRemove) {
|
||||
newFiles.remove(personId);
|
||||
}
|
||||
|
||||
// Add files which are needed
|
||||
for (final personId in toAdd) {
|
||||
newFiles[personId] = (0, {});
|
||||
}
|
||||
|
||||
return SmartAlbumConfig(
|
||||
collectionId: collectionId,
|
||||
personIDs: newPersonsIds,
|
||||
addedFiles: newFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SmartAlbumConfig> addFiles(
|
||||
String personId,
|
||||
int updatedAt,
|
||||
Set<int> fileId,
|
||||
) async {
|
||||
if (!addedFiles.containsKey(personId)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
final newFiles = Map<String, (int, Set<int>)>.from(addedFiles);
|
||||
newFiles[personId] = (
|
||||
updatedAt,
|
||||
newFiles[personId]!.$2.union(fileId),
|
||||
);
|
||||
return SmartAlbumConfig(
|
||||
collectionId: collectionId,
|
||||
personIDs: personIDs,
|
||||
addedFiles: newFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setStringList(
|
||||
"${_personIdsKey}_$collectionId",
|
||||
personIDs.toList(),
|
||||
);
|
||||
|
||||
await prefs.setString(
|
||||
"${_addedFilesKey}_$collectionId",
|
||||
addedFiles.entries
|
||||
.map((e) => "${e.key}:${e.value.$1}|${e.value.$2.join(',')}")
|
||||
.join(';'),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<SmartAlbumConfig> loadConfig(int collectionId) async {
|
||||
final personIDs =
|
||||
prefs.getStringList("${_personIdsKey}_$collectionId") ?? [];
|
||||
final addedFilesString =
|
||||
prefs.getString("${_addedFilesKey}_$collectionId") ?? "";
|
||||
|
||||
final addedFiles = <String, (int, Set<int>)>{};
|
||||
if (addedFilesString.isNotEmpty) {
|
||||
for (final entry in addedFilesString.split(';')) {
|
||||
final parts = entry.split(':');
|
||||
if (parts.length == 2) {
|
||||
addedFiles[parts[0]] = (
|
||||
int.parse(parts[1].split('|')[0]),
|
||||
parts[1].split('|')[1].split(',').map(int.parse).toSet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SmartAlbumConfig(
|
||||
collectionId: collectionId,
|
||||
personIDs: personIDs.toSet(),
|
||||
addedFiles: addedFiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _lock = Lock();
|
||||
|
||||
Future<void> syncSmartAlbums() async {
|
||||
await _lock.synchronized(() async {
|
||||
// get all collections
|
||||
final collections = CollectionsService.instance.nonHiddenOwnedCollections();
|
||||
for (final collectionId in collections) {
|
||||
final config = await SmartAlbumConfig.loadConfig(collectionId);
|
||||
|
||||
if (config.personIDs.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final personId in config.personIDs) {
|
||||
final person =
|
||||
await entityService.getEntity(EntityType.cgroup, personId);
|
||||
|
||||
if (person == null ||
|
||||
config.addedFiles[personId]?.$1 == null ||
|
||||
(person.updatedAt <= config.addedFiles[personId]!.$1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final files =
|
||||
(await SearchService.instance.getClusterFilesForPersonID(personId))
|
||||
.entries
|
||||
.expand((e) => e.value)
|
||||
.toSet();
|
||||
|
||||
final toBeSynced =
|
||||
files.difference(config.addedFiles[personId]?.$2 ?? {});
|
||||
|
||||
if (toBeSynced.isNotEmpty) {
|
||||
final CollectionActions collectionActions =
|
||||
CollectionActions(CollectionsService.instance);
|
||||
final result = await collectionActions.addToCollection(
|
||||
null,
|
||||
collectionId,
|
||||
false,
|
||||
selectedFiles: toBeSynced.toList(),
|
||||
);
|
||||
if (result) {
|
||||
final newConfig = await config.addFiles(
|
||||
personId,
|
||||
person.updatedAt,
|
||||
toBeSynced.map((e) => e.uploadedFileID!).toSet(),
|
||||
);
|
||||
await newConfig.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,10 @@ class ServiceLocator {
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferences get prefs {
|
||||
return ServiceLocator.instance.prefs;
|
||||
}
|
||||
|
||||
FlagService? _flagService;
|
||||
|
||||
FlagService get flagService {
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
import 'package:photos/events/sync_status_update_event.dart';
|
||||
import 'package:photos/events/trigger_logout_event.dart';
|
||||
import "package:photos/models/collection/smart_album_config.dart";
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/services/language_service.dart";
|
||||
import 'package:photos/services/notification_service.dart';
|
||||
@@ -199,6 +200,7 @@ class SyncService {
|
||||
if (shouldSync) {
|
||||
await _remoteSyncService.sync();
|
||||
}
|
||||
await syncSmartAlbums();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,14 +183,14 @@ extension CollectionFileActions on CollectionActions {
|
||||
}
|
||||
|
||||
Future<bool> addToCollection(
|
||||
BuildContext context,
|
||||
BuildContext? context,
|
||||
int collectionID,
|
||||
bool showProgressDialog, {
|
||||
List<EnteFile>? selectedFiles,
|
||||
List<SharedMediaFile>? sharedFiles,
|
||||
List<AssetEntity>? picketAssets,
|
||||
}) async {
|
||||
ProgressDialog? dialog = showProgressDialog
|
||||
ProgressDialog? dialog = showProgressDialog && context != null
|
||||
? createProgressDialog(
|
||||
context,
|
||||
S.of(context).uploadingFilesToAlbum,
|
||||
@@ -246,7 +246,7 @@ extension CollectionFileActions on CollectionActions {
|
||||
final Collection? c =
|
||||
CollectionsService.instance.getCollectionByID(collectionID);
|
||||
if (c != null && c.owner.id != currentUserID) {
|
||||
if (!showProgressDialog) {
|
||||
if (!showProgressDialog && context != null) {
|
||||
dialog = createProgressDialog(
|
||||
context,
|
||||
S.of(context).uploadingFilesToAlbum,
|
||||
@@ -291,7 +291,9 @@ extension CollectionFileActions on CollectionActions {
|
||||
} catch (e, s) {
|
||||
logger.severe("Failed to add to album", e, s);
|
||||
await dialog?.hide();
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
if (context != null) {
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import "dart:async";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/collection/smart_album_config.dart";
|
||||
import "package:photos/models/selected_people.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import 'package:photos/ui/components/title_bar_title_widget.dart';
|
||||
import 'package:photos/ui/components/title_bar_widget.dart';
|
||||
import "package:photos/ui/viewer/search/result/people_section_all_page.dart"
|
||||
show PeopleSectionAllWidget;
|
||||
|
||||
class SmartAlbumPeople extends StatefulWidget {
|
||||
const SmartAlbumPeople({
|
||||
super.key,
|
||||
required this.collectionId,
|
||||
});
|
||||
|
||||
final int collectionId;
|
||||
|
||||
@override
|
||||
State<SmartAlbumPeople> createState() => _SmartAlbumPeopleState();
|
||||
}
|
||||
|
||||
class _SmartAlbumPeopleState extends State<SmartAlbumPeople> {
|
||||
final _selectedPeople = SelectedPeople();
|
||||
late SmartAlbumConfig currentConfig;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getSelections();
|
||||
}
|
||||
|
||||
Future<void> getSelections() async {
|
||||
currentConfig = await SmartAlbumConfig.loadConfig(widget.collectionId);
|
||||
|
||||
_selectedPeople.select(currentConfig.personIDs);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
bottomNavigationBar: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
8,
|
||||
16,
|
||||
8 + MediaQuery.viewPaddingOf(context).bottom,
|
||||
),
|
||||
child: ListenableBuilder(
|
||||
listenable: _selectedPeople,
|
||||
builder: (context, _) {
|
||||
return ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
buttonSize: ButtonSize.large,
|
||||
labelText: S.of(context).save,
|
||||
shouldSurfaceExecutionStates: false,
|
||||
isDisabled: _selectedPeople.personIds.isEmpty,
|
||||
onTap: _selectedPeople.personIds.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
isLoading = true;
|
||||
if (mounted) setState(() {});
|
||||
try {
|
||||
final newConfig = await currentConfig.getUpdatedConfig(
|
||||
_selectedPeople.personIds,
|
||||
);
|
||||
await newConfig.saveConfig();
|
||||
syncSmartAlbums().ignore();
|
||||
Navigator.pop(context);
|
||||
} catch (_) {
|
||||
isLoading = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
TitleBarWidget(
|
||||
flexibleSpaceTitle: TitleBarTitleWidget(
|
||||
title: S.of(context).people,
|
||||
),
|
||||
expandedHeight: MediaQuery.textScalerOf(context).scale(120),
|
||||
flexibleSpaceCaption: S.of(context).peopleWidgetDesc,
|
||||
actionIcons: [
|
||||
IconButtonWidget(
|
||||
icon: Icons.close_outlined,
|
||||
iconButtonType: IconButtonType.secondary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverFillRemaining(
|
||||
child: PeopleSectionAllWidget(
|
||||
selectedPeople: _selectedPeople,
|
||||
namedOnly: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isLoading)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import "package:photos/theme/colors.dart";
|
||||
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
|
||||
import "package:photos/ui/cast/auto.dart";
|
||||
import "package:photos/ui/cast/choose.dart";
|
||||
import "package:photos/ui/collections/album/smart_album_people.dart";
|
||||
import "package:photos/ui/common/popup_item.dart";
|
||||
import "package:photos/ui/common/web_page.dart";
|
||||
import 'package:photos/ui/components/action_sheet_widget.dart';
|
||||
@@ -399,6 +400,26 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.collection != null) {
|
||||
actions.add(
|
||||
Tooltip(
|
||||
message: S.of(context).goToSettings,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () async {
|
||||
await routeToPage(
|
||||
context,
|
||||
SmartAlbumPeople(
|
||||
collectionId: widget.collection!.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (galleryType.isSharable() && !widget.isFromCollectPhotos) {
|
||||
actions.add(
|
||||
Tooltip(
|
||||
|
||||
Reference in New Issue
Block a user