mirror of
https://github.com/immich-app/immich
synced 2025-06-08 11:41:13 +00:00
feat(mobile): add album description functionality (#18886)
* feat(mobile): add album description functionality - Introduced a new optional `description` field in the `Album` entity. - Updated `AlbumViewerPageState` to manage `editDescriptionText`. - Created `AlbumDescription` and `AlbumViewerEditableDescription` widgets for displaying and editing album descriptions. - Enhanced `CreateAlbumPage` to include a description input field. - Implemented backend support for updating album descriptions in `AlbumApiRepository` and `AlbumService`. - Updated sync logic to handle album descriptions during data synchronization. - Adjusted UI components to accommodate the new description feature. * fix dart analysis error * remove comment that shouldn't be there * Album header styling * fix: disable edit after album creation --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
19ff39c2b9
commit
5d0ad853f4
@ -19,6 +19,7 @@ class Album {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.modifiedAt,
|
required this.modifiedAt,
|
||||||
|
this.description,
|
||||||
this.startDate,
|
this.startDate,
|
||||||
this.endDate,
|
this.endDate,
|
||||||
this.lastModifiedAssetTimestamp,
|
this.lastModifiedAssetTimestamp,
|
||||||
@ -34,6 +35,7 @@ class Album {
|
|||||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||||
String? localId;
|
String? localId;
|
||||||
String name;
|
String name;
|
||||||
|
String? description;
|
||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
DateTime modifiedAt;
|
DateTime modifiedAt;
|
||||||
DateTime? startDate;
|
DateTime? startDate;
|
||||||
@ -108,6 +110,7 @@ class Album {
|
|||||||
remoteId == other.remoteId &&
|
remoteId == other.remoteId &&
|
||||||
localId == other.localId &&
|
localId == other.localId &&
|
||||||
name == other.name &&
|
name == other.name &&
|
||||||
|
description == other.description &&
|
||||||
createdAt.isAtSameMomentAs(other.createdAt) &&
|
createdAt.isAtSameMomentAs(other.createdAt) &&
|
||||||
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
|
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
|
||||||
isAtSameMomentAs(startDate, other.startDate) &&
|
isAtSameMomentAs(startDate, other.startDate) &&
|
||||||
@ -135,6 +138,7 @@ class Album {
|
|||||||
modifiedAt.hashCode ^
|
modifiedAt.hashCode ^
|
||||||
startDate.hashCode ^
|
startDate.hashCode ^
|
||||||
endDate.hashCode ^
|
endDate.hashCode ^
|
||||||
|
description.hashCode ^
|
||||||
lastModifiedAssetTimestamp.hashCode ^
|
lastModifiedAssetTimestamp.hashCode ^
|
||||||
shared.hashCode ^
|
shared.hashCode ^
|
||||||
activityEnabled.hashCode ^
|
activityEnabled.hashCode ^
|
||||||
@ -150,6 +154,7 @@ class Album {
|
|||||||
name: dto.albumName,
|
name: dto.albumName,
|
||||||
createdAt: dto.createdAt,
|
createdAt: dto.createdAt,
|
||||||
modifiedAt: dto.updatedAt,
|
modifiedAt: dto.updatedAt,
|
||||||
|
description: dto.description,
|
||||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||||
shared: dto.shared,
|
shared: dto.shared,
|
||||||
startDate: dto.startDate,
|
startDate: dto.startDate,
|
||||||
@ -184,7 +189,8 @@ class Album {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => name;
|
String toString() =>
|
||||||
|
'remoteId: $remoteId name: $name description: $description';
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AssetsHelper on IsarCollection<Album> {
|
extension AssetsHelper on IsarCollection<Album> {
|
||||||
|
269
mobile/lib/entities/album.entity.g.dart
generated
269
mobile/lib/entities/album.entity.g.dart
generated
@ -27,49 +27,54 @@ const AlbumSchema = CollectionSchema(
|
|||||||
name: r'createdAt',
|
name: r'createdAt',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'endDate': PropertySchema(
|
r'description': PropertySchema(
|
||||||
id: 2,
|
id: 2,
|
||||||
|
name: r'description',
|
||||||
|
type: IsarType.string,
|
||||||
|
),
|
||||||
|
r'endDate': PropertySchema(
|
||||||
|
id: 3,
|
||||||
name: r'endDate',
|
name: r'endDate',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'lastModifiedAssetTimestamp': PropertySchema(
|
r'lastModifiedAssetTimestamp': PropertySchema(
|
||||||
id: 3,
|
id: 4,
|
||||||
name: r'lastModifiedAssetTimestamp',
|
name: r'lastModifiedAssetTimestamp',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'localId': PropertySchema(
|
r'localId': PropertySchema(
|
||||||
id: 4,
|
id: 5,
|
||||||
name: r'localId',
|
name: r'localId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'modifiedAt': PropertySchema(
|
r'modifiedAt': PropertySchema(
|
||||||
id: 5,
|
id: 6,
|
||||||
name: r'modifiedAt',
|
name: r'modifiedAt',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'name': PropertySchema(
|
r'name': PropertySchema(
|
||||||
id: 6,
|
id: 7,
|
||||||
name: r'name',
|
name: r'name',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'remoteId': PropertySchema(
|
r'remoteId': PropertySchema(
|
||||||
id: 7,
|
id: 8,
|
||||||
name: r'remoteId',
|
name: r'remoteId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'shared': PropertySchema(
|
r'shared': PropertySchema(
|
||||||
id: 8,
|
id: 9,
|
||||||
name: r'shared',
|
name: r'shared',
|
||||||
type: IsarType.bool,
|
type: IsarType.bool,
|
||||||
),
|
),
|
||||||
r'sortOrder': PropertySchema(
|
r'sortOrder': PropertySchema(
|
||||||
id: 9,
|
id: 10,
|
||||||
name: r'sortOrder',
|
name: r'sortOrder',
|
||||||
type: IsarType.byte,
|
type: IsarType.byte,
|
||||||
enumMap: _AlbumsortOrderEnumValueMap,
|
enumMap: _AlbumsortOrderEnumValueMap,
|
||||||
),
|
),
|
||||||
r'startDate': PropertySchema(
|
r'startDate': PropertySchema(
|
||||||
id: 10,
|
id: 11,
|
||||||
name: r'startDate',
|
name: r'startDate',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
)
|
)
|
||||||
@ -146,6 +151,12 @@ int _albumEstimateSize(
|
|||||||
Map<Type, List<int>> allOffsets,
|
Map<Type, List<int>> allOffsets,
|
||||||
) {
|
) {
|
||||||
var bytesCount = offsets.last;
|
var bytesCount = offsets.last;
|
||||||
|
{
|
||||||
|
final value = object.description;
|
||||||
|
if (value != null) {
|
||||||
|
bytesCount += 3 + value.length * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
final value = object.localId;
|
final value = object.localId;
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@ -170,15 +181,16 @@ void _albumSerialize(
|
|||||||
) {
|
) {
|
||||||
writer.writeBool(offsets[0], object.activityEnabled);
|
writer.writeBool(offsets[0], object.activityEnabled);
|
||||||
writer.writeDateTime(offsets[1], object.createdAt);
|
writer.writeDateTime(offsets[1], object.createdAt);
|
||||||
writer.writeDateTime(offsets[2], object.endDate);
|
writer.writeString(offsets[2], object.description);
|
||||||
writer.writeDateTime(offsets[3], object.lastModifiedAssetTimestamp);
|
writer.writeDateTime(offsets[3], object.endDate);
|
||||||
writer.writeString(offsets[4], object.localId);
|
writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp);
|
||||||
writer.writeDateTime(offsets[5], object.modifiedAt);
|
writer.writeString(offsets[5], object.localId);
|
||||||
writer.writeString(offsets[6], object.name);
|
writer.writeDateTime(offsets[6], object.modifiedAt);
|
||||||
writer.writeString(offsets[7], object.remoteId);
|
writer.writeString(offsets[7], object.name);
|
||||||
writer.writeBool(offsets[8], object.shared);
|
writer.writeString(offsets[8], object.remoteId);
|
||||||
writer.writeByte(offsets[9], object.sortOrder.index);
|
writer.writeBool(offsets[9], object.shared);
|
||||||
writer.writeDateTime(offsets[10], object.startDate);
|
writer.writeByte(offsets[10], object.sortOrder.index);
|
||||||
|
writer.writeDateTime(offsets[11], object.startDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Album _albumDeserialize(
|
Album _albumDeserialize(
|
||||||
@ -190,16 +202,18 @@ Album _albumDeserialize(
|
|||||||
final object = Album(
|
final object = Album(
|
||||||
activityEnabled: reader.readBool(offsets[0]),
|
activityEnabled: reader.readBool(offsets[0]),
|
||||||
createdAt: reader.readDateTime(offsets[1]),
|
createdAt: reader.readDateTime(offsets[1]),
|
||||||
endDate: reader.readDateTimeOrNull(offsets[2]),
|
description: reader.readStringOrNull(offsets[2]),
|
||||||
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[3]),
|
endDate: reader.readDateTimeOrNull(offsets[3]),
|
||||||
localId: reader.readStringOrNull(offsets[4]),
|
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]),
|
||||||
modifiedAt: reader.readDateTime(offsets[5]),
|
localId: reader.readStringOrNull(offsets[5]),
|
||||||
name: reader.readString(offsets[6]),
|
modifiedAt: reader.readDateTime(offsets[6]),
|
||||||
remoteId: reader.readStringOrNull(offsets[7]),
|
name: reader.readString(offsets[7]),
|
||||||
shared: reader.readBool(offsets[8]),
|
remoteId: reader.readStringOrNull(offsets[8]),
|
||||||
sortOrder: _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[9])] ??
|
shared: reader.readBool(offsets[9]),
|
||||||
|
sortOrder:
|
||||||
|
_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ??
|
||||||
SortOrder.desc,
|
SortOrder.desc,
|
||||||
startDate: reader.readDateTimeOrNull(offsets[10]),
|
startDate: reader.readDateTimeOrNull(offsets[11]),
|
||||||
);
|
);
|
||||||
object.id = id;
|
object.id = id;
|
||||||
return object;
|
return object;
|
||||||
@ -217,23 +231,25 @@ P _albumDeserializeProp<P>(
|
|||||||
case 1:
|
case 1:
|
||||||
return (reader.readDateTime(offset)) as P;
|
return (reader.readDateTime(offset)) as P;
|
||||||
case 2:
|
case 2:
|
||||||
return (reader.readDateTimeOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 3:
|
case 3:
|
||||||
return (reader.readDateTimeOrNull(offset)) as P;
|
return (reader.readDateTimeOrNull(offset)) as P;
|
||||||
case 4:
|
case 4:
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readDateTimeOrNull(offset)) as P;
|
||||||
case 5:
|
case 5:
|
||||||
return (reader.readDateTime(offset)) as P;
|
|
||||||
case 6:
|
|
||||||
return (reader.readString(offset)) as P;
|
|
||||||
case 7:
|
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
|
case 6:
|
||||||
|
return (reader.readDateTime(offset)) as P;
|
||||||
|
case 7:
|
||||||
|
return (reader.readString(offset)) as P;
|
||||||
case 8:
|
case 8:
|
||||||
return (reader.readBool(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
case 9:
|
case 9:
|
||||||
|
return (reader.readBool(offset)) as P;
|
||||||
|
case 10:
|
||||||
return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ??
|
return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||||
SortOrder.desc) as P;
|
SortOrder.desc) as P;
|
||||||
case 10:
|
case 11:
|
||||||
return (reader.readDateTimeOrNull(offset)) as P;
|
return (reader.readDateTimeOrNull(offset)) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
@ -535,6 +551,152 @@ extension AlbumQueryFilter on QueryBuilder<Album, Album, QFilterCondition> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNull() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(const FilterCondition.isNull(
|
||||||
|
property: r'description',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNotNull() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||||
|
property: r'description',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionEqualTo(
|
||||||
|
String? value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'description',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionGreaterThan(
|
||||||
|
String? value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'description',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionLessThan(
|
||||||
|
String? value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'description',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionBetween(
|
||||||
|
String? lower,
|
||||||
|
String? upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'description',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionStartsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.startsWith(
|
||||||
|
property: r'description',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionEndsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.endsWith(
|
||||||
|
property: r'description',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionContains(
|
||||||
|
String value,
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.contains(
|
||||||
|
property: r'description',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionMatches(
|
||||||
|
String pattern,
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.matches(
|
||||||
|
property: r'description',
|
||||||
|
wildcard: pattern,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'description',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterFilterCondition> descriptionIsNotEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
property: r'description',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QAfterFilterCondition> endDateIsNull() {
|
QueryBuilder<Album, Album, QAfterFilterCondition> endDateIsNull() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addFilterCondition(const FilterCondition.isNull(
|
return query.addFilterCondition(const FilterCondition.isNull(
|
||||||
@ -1502,6 +1664,18 @@ extension AlbumQuerySortBy on QueryBuilder<Album, Album, QSortBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> sortByDescription() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'description', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> sortByDescriptionDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'description', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QAfterSortBy> sortByEndDate() {
|
QueryBuilder<Album, Album, QAfterSortBy> sortByEndDate() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'endDate', Sort.asc);
|
return query.addSortBy(r'endDate', Sort.asc);
|
||||||
@ -1637,6 +1811,18 @@ extension AlbumQuerySortThenBy on QueryBuilder<Album, Album, QSortThenBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> thenByDescription() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'description', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QAfterSortBy> thenByDescriptionDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'description', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QAfterSortBy> thenByEndDate() {
|
QueryBuilder<Album, Album, QAfterSortBy> thenByEndDate() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'endDate', Sort.asc);
|
return query.addSortBy(r'endDate', Sort.asc);
|
||||||
@ -1772,6 +1958,13 @@ extension AlbumQueryWhereDistinct on QueryBuilder<Album, Album, QDistinct> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, Album, QDistinct> distinctByDescription(
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'description', caseSensitive: caseSensitive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, Album, QDistinct> distinctByEndDate() {
|
QueryBuilder<Album, Album, QDistinct> distinctByEndDate() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addDistinctBy(r'endDate');
|
return query.addDistinctBy(r'endDate');
|
||||||
@ -1849,6 +2042,12 @@ extension AlbumQueryProperty on QueryBuilder<Album, Album, QQueryProperty> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Album, String?, QQueryOperations> descriptionProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'description');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Album, DateTime?, QQueryOperations> endDateProperty() {
|
QueryBuilder<Album, DateTime?, QQueryOperations> endDateProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'endDate');
|
return query.addPropertyName(r'endDate');
|
||||||
|
@ -3,18 +3,23 @@ import 'dart:convert';
|
|||||||
class AlbumViewerPageState {
|
class AlbumViewerPageState {
|
||||||
final bool isEditAlbum;
|
final bool isEditAlbum;
|
||||||
final String editTitleText;
|
final String editTitleText;
|
||||||
|
final String editDescriptionText;
|
||||||
|
|
||||||
AlbumViewerPageState({
|
AlbumViewerPageState({
|
||||||
required this.isEditAlbum,
|
required this.isEditAlbum,
|
||||||
required this.editTitleText,
|
required this.editTitleText,
|
||||||
|
required this.editDescriptionText,
|
||||||
});
|
});
|
||||||
|
|
||||||
AlbumViewerPageState copyWith({
|
AlbumViewerPageState copyWith({
|
||||||
bool? isEditAlbum,
|
bool? isEditAlbum,
|
||||||
String? editTitleText,
|
String? editTitleText,
|
||||||
|
String? editDescriptionText,
|
||||||
}) {
|
}) {
|
||||||
return AlbumViewerPageState(
|
return AlbumViewerPageState(
|
||||||
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
|
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
|
||||||
editTitleText: editTitleText ?? this.editTitleText,
|
editTitleText: editTitleText ?? this.editTitleText,
|
||||||
|
editDescriptionText: editDescriptionText ?? this.editDescriptionText,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +28,7 @@ class AlbumViewerPageState {
|
|||||||
|
|
||||||
result.addAll({'isEditAlbum': isEditAlbum});
|
result.addAll({'isEditAlbum': isEditAlbum});
|
||||||
result.addAll({'editTitleText': editTitleText});
|
result.addAll({'editTitleText': editTitleText});
|
||||||
|
result.addAll({'editDescriptionText': editDescriptionText});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -31,6 +37,7 @@ class AlbumViewerPageState {
|
|||||||
return AlbumViewerPageState(
|
return AlbumViewerPageState(
|
||||||
isEditAlbum: map['isEditAlbum'] ?? false,
|
isEditAlbum: map['isEditAlbum'] ?? false,
|
||||||
editTitleText: map['editTitleText'] ?? '',
|
editTitleText: map['editTitleText'] ?? '',
|
||||||
|
editDescriptionText: map['editDescriptionText'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +48,7 @@ class AlbumViewerPageState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
|
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
@ -49,9 +56,13 @@ class AlbumViewerPageState {
|
|||||||
|
|
||||||
return other is AlbumViewerPageState &&
|
return other is AlbumViewerPageState &&
|
||||||
other.isEditAlbum == isEditAlbum &&
|
other.isEditAlbum == isEditAlbum &&
|
||||||
other.editTitleText == editTitleText;
|
other.editTitleText == editTitleText &&
|
||||||
|
other.editDescriptionText == editDescriptionText;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode;
|
int get hashCode =>
|
||||||
|
isEditAlbum.hashCode ^
|
||||||
|
editTitleText.hashCode ^
|
||||||
|
editDescriptionText.hashCode;
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,9 @@ class AlbumControlButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 36,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
children: [
|
||||||
|
@ -30,15 +30,12 @@ class AlbumDateRange extends ConsumerWidget {
|
|||||||
final (startDate, endDate, shared) = data;
|
final (startDate, endDate, shared) = data;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: shared
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
? const EdgeInsets.only(
|
|
||||||
left: 16.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
)
|
|
||||||
: const EdgeInsets.only(left: 16.0, bottom: 8.0),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
_getDateRangeText(startDate, endDate),
|
_getDateRangeText(startDate, endDate),
|
||||||
style: context.textTheme.labelLarge,
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
45
mobile/lib/pages/album/album_description.dart
Normal file
45
mobile/lib/pages/album/album_description.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
|
||||||
|
class AlbumDescription extends ConsumerWidget {
|
||||||
|
const AlbumDescription({super.key, required this.descriptionFocusNode});
|
||||||
|
|
||||||
|
final FocusNode descriptionFocusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userId = ref.watch(authProvider).userId;
|
||||||
|
final (isOwner, isRemote, albumDescription) = ref.watch(
|
||||||
|
currentAlbumProvider.select((album) {
|
||||||
|
if (album == null) {
|
||||||
|
return const (false, false, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (album.ownerId == userId, album.isRemote, album.description);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isOwner && isRemote) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8),
|
||||||
|
child: AlbumViewerEditableDescription(
|
||||||
|
albumDescription: albumDescription ?? 'add_a_description'.tr(),
|
||||||
|
descriptionFocusNode: descriptionFocusNode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 8),
|
||||||
|
child: Text(
|
||||||
|
albumDescription ?? 'add_a_description'.tr(),
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
return Padding(
|
return Padding(
|
||||||
|
@ -19,7 +19,11 @@ class AlbumTitle extends ConsumerWidget {
|
|||||||
return const (false, false, '');
|
return const (false, false, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (album.ownerId == userId, album.isRemote, album.name);
|
return (
|
||||||
|
album.ownerId == userId,
|
||||||
|
album.isRemote,
|
||||||
|
album.name,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -35,7 +39,12 @@ class AlbumTitle extends ConsumerWidget {
|
|||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 8),
|
padding: const EdgeInsets.only(left: 16, right: 8),
|
||||||
child: Text(albumName, style: context.textTheme.headlineMedium),
|
child: Text(
|
||||||
|
albumName,
|
||||||
|
style: context.textTheme.headlineLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_control_button.dart';
|
import 'package:immich_mobile/pages/album/album_control_button.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_date_range.dart';
|
import 'package:immich_mobile/pages/album/album_date_range.dart';
|
||||||
|
import 'package:immich_mobile/pages/album/album_description.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
|
import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_title.dart';
|
import 'package:immich_mobile/pages/album/album_title.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
@ -36,6 +37,7 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final titleFocusNode = useFocusNode();
|
final titleFocusNode = useFocusNode();
|
||||||
|
final descriptionFocusNode = useFocusNode();
|
||||||
final userId = ref.watch(authProvider).userId;
|
final userId = ref.watch(authProvider).userId;
|
||||||
final isMultiselecting = ref.watch(multiselectProvider);
|
final isMultiselecting = ref.watch(multiselectProvider);
|
||||||
final isProcessing = useProcessingOverlay();
|
final isProcessing = useProcessingOverlay();
|
||||||
@ -106,15 +108,34 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
MultiselectGrid(
|
MultiselectGrid(
|
||||||
key: const ValueKey("albumViewerMultiselectGrid"),
|
key: const ValueKey("albumViewerMultiselectGrid"),
|
||||||
renderListProvider: albumTimelineProvider(album.id),
|
renderListProvider: albumTimelineProvider(album.id),
|
||||||
topWidget: Column(
|
topWidget: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
context.primaryColor.withValues(alpha: 0.04),
|
||||||
|
context.primaryColor.withValues(alpha: 0.02),
|
||||||
|
Colors.orange.withValues(alpha: 0.02),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
stops: const [0.0, 0.3, 0.7, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
const AlbumDateRange(),
|
||||||
AlbumTitle(
|
AlbumTitle(
|
||||||
key: const ValueKey("albumTitle"),
|
key: const ValueKey("albumTitle"),
|
||||||
titleFocusNode: titleFocusNode,
|
titleFocusNode: titleFocusNode,
|
||||||
),
|
),
|
||||||
const AlbumDateRange(),
|
AlbumDescription(
|
||||||
|
key: const ValueKey("albumDescription"),
|
||||||
|
descriptionFocusNode: descriptionFocusNode,
|
||||||
|
),
|
||||||
const AlbumSharedUserIcons(),
|
const AlbumSharedUserIcons(),
|
||||||
if (album.isRemote)
|
if (album.isRemote)
|
||||||
AlbumControlButton(
|
AlbumControlButton(
|
||||||
@ -122,8 +143,10 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
onAddPhotosPressed: onAddPhotosPressed,
|
onAddPhotosPressed: onAddPhotosPressed,
|
||||||
onAddUsersPressed: onAddUsersPressed,
|
onAddUsersPressed: onAddUsersPressed,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||||
editEnabled: album.ownerId == userId,
|
editEnabled: album.ownerId == userId,
|
||||||
),
|
),
|
||||||
@ -136,6 +159,7 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
child: AlbumViewerAppbar(
|
child: AlbumViewerAppbar(
|
||||||
key: const ValueKey("albumViewerAppbar"),
|
key: const ValueKey("albumViewerAppbar"),
|
||||||
titleFocusNode: titleFocusNode,
|
titleFocusNode: titleFocusNode,
|
||||||
|
descriptionFocusNode: descriptionFocusNode,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
onAddPhotos: onAddPhotosPressed,
|
onAddPhotos: onAddPhotosPressed,
|
||||||
onAddUsers: onAddUsersPressed,
|
onAddUsers: onAddUsersPressed,
|
||||||
|
@ -8,9 +8,11 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
|
||||||
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
|
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
|
||||||
|
import 'package:immich_mobile/widgets/album/album_viewer_editable_description.dart';
|
||||||
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -28,6 +30,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
final albumTitleController =
|
final albumTitleController =
|
||||||
useTextEditingController.fromValue(TextEditingValue.empty);
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final albumTitleTextFieldFocusNode = useFocusNode();
|
final albumTitleTextFieldFocusNode = useFocusNode();
|
||||||
|
final albumDescriptionTextFieldFocusNode = useFocusNode();
|
||||||
final isAlbumTitleTextFieldFocus = useState(false);
|
final isAlbumTitleTextFieldFocus = useState(false);
|
||||||
final isAlbumTitleEmpty = useState(true);
|
final isAlbumTitleEmpty = useState(true);
|
||||||
final selectedAssets = useState<Set<Asset>>(
|
final selectedAssets = useState<Set<Asset>>(
|
||||||
@ -36,6 +39,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
void onBackgroundTapped() {
|
void onBackgroundTapped() {
|
||||||
albumTitleTextFieldFocusNode.unfocus();
|
albumTitleTextFieldFocusNode.unfocus();
|
||||||
|
albumDescriptionTextFieldFocusNode.unfocus();
|
||||||
isAlbumTitleTextFieldFocus.value = false;
|
isAlbumTitleTextFieldFocus.value = false;
|
||||||
|
|
||||||
if (albumTitleController.text.isEmpty) {
|
if (albumTitleController.text.isEmpty) {
|
||||||
@ -77,6 +81,19 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildDescriptionInputField() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 10,
|
||||||
|
left: 10,
|
||||||
|
),
|
||||||
|
child: AlbumViewerEditableDescription(
|
||||||
|
albumDescription: '',
|
||||||
|
descriptionFocusNode: albumDescriptionTextFieldFocusNode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
buildTitle() {
|
buildTitle() {
|
||||||
if (selectedAssets.value.isEmpty) {
|
if (selectedAssets.value.isEmpty) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
@ -178,18 +195,18 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
return const SliverToBoxAdapter();
|
return const SliverToBoxAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
createNonSharedAlbum() async {
|
Future<void> createAlbum() async {
|
||||||
onBackgroundTapped();
|
onBackgroundTapped();
|
||||||
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||||
ref.watch(albumTitleProvider),
|
ref.read(albumTitleProvider),
|
||||||
selectedAssets.value,
|
selectedAssets.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newAlbum != null) {
|
if (newAlbum != null) {
|
||||||
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
selectedAssets.value = {};
|
selectedAssets.value = {};
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.read(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
ref.read(albumViewerProvider.notifier).disableEditAlbum();
|
||||||
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
|
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,9 +228,8 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
).tr(),
|
).tr(),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: albumTitleController.text.isNotEmpty
|
onPressed:
|
||||||
? createNonSharedAlbum
|
albumTitleController.text.isNotEmpty ? createAlbum : null,
|
||||||
: null,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'create'.tr(),
|
'create'.tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -237,10 +253,11 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
pinned: true,
|
pinned: true,
|
||||||
floating: false,
|
floating: false,
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(96.0),
|
preferredSize: const Size.fromHeight(125.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
buildTitleInputField(),
|
buildTitleInputField(),
|
||||||
|
buildDescriptionInputField(),
|
||||||
if (selectedAssets.value.isNotEmpty) buildControlButton(),
|
if (selectedAssets.value.isNotEmpty) buildControlButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,13 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
|||||||
|
|
||||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
AlbumViewerNotifier(this.ref)
|
AlbumViewerNotifier(this.ref)
|
||||||
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
|
: super(
|
||||||
|
AlbumViewerPageState(
|
||||||
|
editTitleText: "",
|
||||||
|
isEditAlbum: false,
|
||||||
|
editDescriptionText: "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
@ -21,12 +27,24 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
|||||||
state = state.copyWith(editTitleText: newTitle);
|
state = state.copyWith(editTitleText: newTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setEditDescriptionText(String newDescription) {
|
||||||
|
state = state.copyWith(editDescriptionText: newDescription);
|
||||||
|
}
|
||||||
|
|
||||||
void remoteEditTitleText() {
|
void remoteEditTitleText() {
|
||||||
state = state.copyWith(editTitleText: "");
|
state = state.copyWith(editTitleText: "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void remoteEditDescriptionText() {
|
||||||
|
state = state.copyWith(editDescriptionText: "");
|
||||||
|
}
|
||||||
|
|
||||||
void resetState() {
|
void resetState() {
|
||||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
state = state.copyWith(
|
||||||
|
editTitleText: "",
|
||||||
|
isEditAlbum: false,
|
||||||
|
editDescriptionText: "",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeAlbumTitle(
|
Future<bool> changeAlbumTitle(
|
||||||
@ -46,6 +64,28 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
|||||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> changeAlbumDescription(
|
||||||
|
Album album,
|
||||||
|
String newAlbumDescription,
|
||||||
|
) async {
|
||||||
|
AlbumService service = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
|
bool isSuccess = await service.changeDescriptionAlbum(
|
||||||
|
album,
|
||||||
|
newAlbumDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(editDescriptionText: "", isEditAlbum: false);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final albumViewerProvider =
|
final albumViewerProvider =
|
||||||
|
@ -36,6 +36,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
String name, {
|
String name, {
|
||||||
required Iterable<String> assetIds,
|
required Iterable<String> assetIds,
|
||||||
Iterable<String> sharedUserIds = const [],
|
Iterable<String> sharedUserIds = const [],
|
||||||
|
String? description,
|
||||||
}) async {
|
}) async {
|
||||||
final users = sharedUserIds.map(
|
final users = sharedUserIds.map(
|
||||||
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
||||||
@ -44,6 +45,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
_api.createAlbum(
|
_api.createAlbum(
|
||||||
CreateAlbumDto(
|
CreateAlbumDto(
|
||||||
albumName: name,
|
albumName: name,
|
||||||
|
description: description,
|
||||||
assetIds: assetIds.toList(),
|
assetIds: assetIds.toList(),
|
||||||
albumUsers: users.toList(),
|
albumUsers: users.toList(),
|
||||||
),
|
),
|
||||||
@ -161,6 +163,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||||
shared: dto.shared,
|
shared: dto.shared,
|
||||||
startDate: dto.startDate,
|
startDate: dto.startDate,
|
||||||
|
description: dto.description,
|
||||||
endDate: dto.endDate,
|
endDate: dto.endDate,
|
||||||
activityEnabled: dto.isActivityEnabled,
|
activityEnabled: dto.isActivityEnabled,
|
||||||
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
|
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
|
||||||
@ -174,6 +177,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
|||||||
album.sharedUsers.addAll(users.map(entity.User.fromDto));
|
album.sharedUsers.addAll(users.map(entity.User.fromDto));
|
||||||
final assets = dto.assets.map(Asset.remote).toList();
|
final assets = dto.assets.map(Asset.remote).toList();
|
||||||
album.assets.addAll(assets);
|
album.assets.addAll(assets);
|
||||||
|
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -422,6 +422,25 @@ class AlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> changeDescriptionAlbum(
|
||||||
|
Album album,
|
||||||
|
String newAlbumDescription,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
|
album.remoteId!,
|
||||||
|
description: newAlbumDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
album.description = updatedAlbum.description;
|
||||||
|
await _albumRepository.update(album);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error changeDescriptionAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Album?> getAlbumByName(
|
Future<Album?> getAlbumByName(
|
||||||
String name, {
|
String name, {
|
||||||
bool? remote,
|
bool? remote,
|
||||||
|
@ -451,6 +451,7 @@ class SyncService {
|
|||||||
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
|
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
|
||||||
|
|
||||||
album.name = dto.name;
|
album.name = dto.name;
|
||||||
|
album.description = dto.description;
|
||||||
album.shared = dto.shared;
|
album.shared = dto.shared;
|
||||||
album.createdAt = dto.createdAt;
|
album.createdAt = dto.createdAt;
|
||||||
album.modifiedAt = dto.modifiedAt;
|
album.modifiedAt = dto.modifiedAt;
|
||||||
@ -643,6 +644,7 @@ class SyncService {
|
|||||||
toUpdate.isEmpty &&
|
toUpdate.isEmpty &&
|
||||||
toDelete.isEmpty &&
|
toDelete.isEmpty &&
|
||||||
dbAlbum.name == deviceAlbum.name &&
|
dbAlbum.name == deviceAlbum.name &&
|
||||||
|
dbAlbum.description == deviceAlbum.description &&
|
||||||
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
||||||
// changes only affeted excluded albums
|
// changes only affeted excluded albums
|
||||||
_log.info(
|
_log.info(
|
||||||
@ -670,6 +672,7 @@ class SyncService {
|
|||||||
deleteCandidates.addAll(toDelete);
|
deleteCandidates.addAll(toDelete);
|
||||||
existing.addAll(existingInDb);
|
existing.addAll(existingInDb);
|
||||||
dbAlbum.name = deviceAlbum.name;
|
dbAlbum.name = deviceAlbum.name;
|
||||||
|
dbAlbum.description = deviceAlbum.description;
|
||||||
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||||
if (dbAlbum.thumbnail.value != null &&
|
if (dbAlbum.thumbnail.value != null &&
|
||||||
toDelete.contains(dbAlbum.thumbnail.value)) {
|
toDelete.contains(dbAlbum.thumbnail.value)) {
|
||||||
@ -943,6 +946,7 @@ class SyncService {
|
|||||||
Album dbAlbum,
|
Album dbAlbum,
|
||||||
) async {
|
) async {
|
||||||
return deviceAlbum.name != dbAlbum.name ||
|
return deviceAlbum.name != dbAlbum.name ||
|
||||||
|
deviceAlbum.description != dbAlbum.description ||
|
||||||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
||||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||||
@ -1101,6 +1105,7 @@ class SyncService {
|
|||||||
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
||||||
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
|
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
|
||||||
remoteAlbum.name != dbAlbum.name ||
|
remoteAlbum.name != dbAlbum.name ||
|
||||||
|
remoteAlbum.description != dbAlbum.description ||
|
||||||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
|
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
|
||||||
remoteAlbum.shared != dbAlbum.shared ||
|
remoteAlbum.shared != dbAlbum.shared ||
|
||||||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
|
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
|
||||||
|
@ -16,7 +16,7 @@ class AlbumActionFilledButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||||
@ -32,9 +32,7 @@ class AlbumActionFilledButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
labelText,
|
labelText,
|
||||||
style: context.textTheme.labelMedium?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(),
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
|
@ -18,6 +18,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
super.key,
|
super.key,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.titleFocusNode,
|
required this.titleFocusNode,
|
||||||
|
required this.descriptionFocusNode,
|
||||||
this.onAddPhotos,
|
this.onAddPhotos,
|
||||||
this.onAddUsers,
|
this.onAddUsers,
|
||||||
required this.onActivities,
|
required this.onActivities,
|
||||||
@ -25,6 +26,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
|
|
||||||
final String userId;
|
final String userId;
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
|
final FocusNode descriptionFocusNode;
|
||||||
final void Function()? onAddPhotos;
|
final void Function()? onAddPhotos;
|
||||||
final void Function()? onAddUsers;
|
final void Function()? onAddUsers;
|
||||||
final void Function() onActivities;
|
final void Function() onActivities;
|
||||||
@ -48,6 +50,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
|
|
||||||
final albumViewer = ref.watch(albumViewerProvider);
|
final albumViewer = ref.watch(albumViewerProvider);
|
||||||
final newAlbumTitle = albumViewer.editTitleText;
|
final newAlbumTitle = albumViewer.editTitleText;
|
||||||
|
final newAlbumDescription = albumViewer.editDescriptionText;
|
||||||
final isEditAlbum = albumViewer.isEditAlbum;
|
final isEditAlbum = albumViewer.isEditAlbum;
|
||||||
|
|
||||||
final comments = album.shared
|
final comments = album.shared
|
||||||
@ -277,10 +280,10 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
if (isEditAlbum) {
|
if (isEditAlbum) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
if (newAlbumTitle.isNotEmpty) {
|
||||||
bool isSuccess = await ref
|
bool isSuccess = await ref
|
||||||
.watch(albumViewerProvider.notifier)
|
.watch(albumViewerProvider.notifier)
|
||||||
.changeAlbumTitle(album, newAlbumTitle);
|
.changeAlbumTitle(album, newAlbumTitle);
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@ -289,8 +292,25 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
titleFocusNode.unfocus();
|
titleFocusNode.unfocus();
|
||||||
|
} else if (newAlbumDescription.isNotEmpty) {
|
||||||
|
bool isSuccessDescription = await ref
|
||||||
|
.watch(albumViewerProvider.notifier)
|
||||||
|
.changeAlbumDescription(album, newAlbumDescription);
|
||||||
|
if (!isSuccessDescription) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "album_viewer_appbar_share_err_description".tr(),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
descriptionFocusNode.unfocus();
|
||||||
|
} else {
|
||||||
|
titleFocusNode.unfocus();
|
||||||
|
descriptionFocusNode.unfocus();
|
||||||
|
ref.read(albumViewerProvider.notifier).disableEditAlbum();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check_rounded),
|
icon: const Icon(Icons.check_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
|
102
mobile/lib/widgets/album/album_viewer_editable_description.dart
Normal file
102
mobile/lib/widgets/album/album_viewer_editable_description.dart
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album_viewer.provider.dart';
|
||||||
|
|
||||||
|
class AlbumViewerEditableDescription extends HookConsumerWidget {
|
||||||
|
final String albumDescription;
|
||||||
|
final FocusNode descriptionFocusNode;
|
||||||
|
const AlbumViewerEditableDescription({
|
||||||
|
super.key,
|
||||||
|
required this.albumDescription,
|
||||||
|
required this.descriptionFocusNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final albumViewerState = ref.watch(albumViewerProvider);
|
||||||
|
|
||||||
|
final descriptionTextEditController = useTextEditingController(
|
||||||
|
text: albumViewerState.isEditAlbum &&
|
||||||
|
albumViewerState.editDescriptionText.isNotEmpty
|
||||||
|
? albumViewerState.editDescriptionText
|
||||||
|
: albumDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
void onFocusModeChange() {
|
||||||
|
if (!descriptionFocusNode.hasFocus &&
|
||||||
|
descriptionTextEditController.text.isEmpty) {
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditDescriptionText("");
|
||||||
|
descriptionTextEditController.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
descriptionFocusNode.addListener(onFocusModeChange);
|
||||||
|
return () {
|
||||||
|
descriptionFocusNode.removeListener(onFocusModeChange);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.watch(albumViewerProvider.notifier)
|
||||||
|
.setEditDescriptionText(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusNode: descriptionFocusNode,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
maxLines: 3,
|
||||||
|
minLines: 1,
|
||||||
|
controller: descriptionTextEditController,
|
||||||
|
onTap: () {
|
||||||
|
context.focusScope.requestFocus(descriptionFocusNode);
|
||||||
|
|
||||||
|
ref
|
||||||
|
.watch(albumViewerProvider.notifier)
|
||||||
|
.setEditDescriptionText(albumDescription);
|
||||||
|
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
||||||
|
|
||||||
|
if (descriptionTextEditController.text == '') {
|
||||||
|
descriptionTextEditController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
suffixIcon: descriptionFocusNode.hasFocus
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
descriptionTextEditController.clear();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
splashRadius: 10,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
enabledBorder: const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
focusedBorder: const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
focusColor: Colors.grey[300],
|
||||||
|
fillColor: context.scaffoldBackgroundColor,
|
||||||
|
filled: descriptionFocusNode.hasFocus,
|
||||||
|
hintText: 'add_a_description'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -52,7 +52,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusNode: titleFocusNode,
|
focusNode: titleFocusNode,
|
||||||
style: context.textTheme.headlineMedium,
|
style: context.textTheme.headlineLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
controller: titleTextEditController,
|
controller: titleTextEditController,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.focusScope.requestFocus(titleFocusNode);
|
context.focusScope.requestFocus(titleFocusNode);
|
||||||
@ -65,8 +67,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
horizontal: 8,
|
||||||
|
vertical: 0,
|
||||||
|
),
|
||||||
suffixIcon: titleFocusNode.hasFocus
|
suffixIcon: titleFocusNode.hasFocus
|
||||||
? IconButton(
|
? IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user