mirror of
https://github.com/immich-app/immich
synced 2025-06-07 16:03:34 +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.createdAt,
|
||||
required this.modifiedAt,
|
||||
this.description,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.lastModifiedAssetTimestamp,
|
||||
@ -34,6 +35,7 @@ class Album {
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? localId;
|
||||
String name;
|
||||
String? description;
|
||||
DateTime createdAt;
|
||||
DateTime modifiedAt;
|
||||
DateTime? startDate;
|
||||
@ -108,6 +110,7 @@ class Album {
|
||||
remoteId == other.remoteId &&
|
||||
localId == other.localId &&
|
||||
name == other.name &&
|
||||
description == other.description &&
|
||||
createdAt.isAtSameMomentAs(other.createdAt) &&
|
||||
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
|
||||
isAtSameMomentAs(startDate, other.startDate) &&
|
||||
@ -135,6 +138,7 @@ class Album {
|
||||
modifiedAt.hashCode ^
|
||||
startDate.hashCode ^
|
||||
endDate.hashCode ^
|
||||
description.hashCode ^
|
||||
lastModifiedAssetTimestamp.hashCode ^
|
||||
shared.hashCode ^
|
||||
activityEnabled.hashCode ^
|
||||
@ -150,6 +154,7 @@ class Album {
|
||||
name: dto.albumName,
|
||||
createdAt: dto.createdAt,
|
||||
modifiedAt: dto.updatedAt,
|
||||
description: dto.description,
|
||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||
shared: dto.shared,
|
||||
startDate: dto.startDate,
|
||||
@ -184,7 +189,8 @@ class Album {
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
String toString() =>
|
||||
'remoteId: $remoteId name: $name description: $description';
|
||||
}
|
||||
|
||||
extension AssetsHelper on IsarCollection<Album> {
|
||||
|
271
mobile/lib/entities/album.entity.g.dart
generated
271
mobile/lib/entities/album.entity.g.dart
generated
@ -27,49 +27,54 @@ const AlbumSchema = CollectionSchema(
|
||||
name: r'createdAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'endDate': PropertySchema(
|
||||
r'description': PropertySchema(
|
||||
id: 2,
|
||||
name: r'description',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'endDate': PropertySchema(
|
||||
id: 3,
|
||||
name: r'endDate',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'lastModifiedAssetTimestamp': PropertySchema(
|
||||
id: 3,
|
||||
id: 4,
|
||||
name: r'lastModifiedAssetTimestamp',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'localId': PropertySchema(
|
||||
id: 4,
|
||||
id: 5,
|
||||
name: r'localId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'modifiedAt': PropertySchema(
|
||||
id: 5,
|
||||
id: 6,
|
||||
name: r'modifiedAt',
|
||||
type: IsarType.dateTime,
|
||||
),
|
||||
r'name': PropertySchema(
|
||||
id: 6,
|
||||
id: 7,
|
||||
name: r'name',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'remoteId': PropertySchema(
|
||||
id: 7,
|
||||
id: 8,
|
||||
name: r'remoteId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'shared': PropertySchema(
|
||||
id: 8,
|
||||
id: 9,
|
||||
name: r'shared',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'sortOrder': PropertySchema(
|
||||
id: 9,
|
||||
id: 10,
|
||||
name: r'sortOrder',
|
||||
type: IsarType.byte,
|
||||
enumMap: _AlbumsortOrderEnumValueMap,
|
||||
),
|
||||
r'startDate': PropertySchema(
|
||||
id: 10,
|
||||
id: 11,
|
||||
name: r'startDate',
|
||||
type: IsarType.dateTime,
|
||||
)
|
||||
@ -146,6 +151,12 @@ int _albumEstimateSize(
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
{
|
||||
final value = object.description;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.localId;
|
||||
if (value != null) {
|
||||
@ -170,15 +181,16 @@ void _albumSerialize(
|
||||
) {
|
||||
writer.writeBool(offsets[0], object.activityEnabled);
|
||||
writer.writeDateTime(offsets[1], object.createdAt);
|
||||
writer.writeDateTime(offsets[2], object.endDate);
|
||||
writer.writeDateTime(offsets[3], object.lastModifiedAssetTimestamp);
|
||||
writer.writeString(offsets[4], object.localId);
|
||||
writer.writeDateTime(offsets[5], object.modifiedAt);
|
||||
writer.writeString(offsets[6], object.name);
|
||||
writer.writeString(offsets[7], object.remoteId);
|
||||
writer.writeBool(offsets[8], object.shared);
|
||||
writer.writeByte(offsets[9], object.sortOrder.index);
|
||||
writer.writeDateTime(offsets[10], object.startDate);
|
||||
writer.writeString(offsets[2], object.description);
|
||||
writer.writeDateTime(offsets[3], object.endDate);
|
||||
writer.writeDateTime(offsets[4], object.lastModifiedAssetTimestamp);
|
||||
writer.writeString(offsets[5], object.localId);
|
||||
writer.writeDateTime(offsets[6], object.modifiedAt);
|
||||
writer.writeString(offsets[7], object.name);
|
||||
writer.writeString(offsets[8], object.remoteId);
|
||||
writer.writeBool(offsets[9], object.shared);
|
||||
writer.writeByte(offsets[10], object.sortOrder.index);
|
||||
writer.writeDateTime(offsets[11], object.startDate);
|
||||
}
|
||||
|
||||
Album _albumDeserialize(
|
||||
@ -190,16 +202,18 @@ Album _albumDeserialize(
|
||||
final object = Album(
|
||||
activityEnabled: reader.readBool(offsets[0]),
|
||||
createdAt: reader.readDateTime(offsets[1]),
|
||||
endDate: reader.readDateTimeOrNull(offsets[2]),
|
||||
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[3]),
|
||||
localId: reader.readStringOrNull(offsets[4]),
|
||||
modifiedAt: reader.readDateTime(offsets[5]),
|
||||
name: reader.readString(offsets[6]),
|
||||
remoteId: reader.readStringOrNull(offsets[7]),
|
||||
shared: reader.readBool(offsets[8]),
|
||||
sortOrder: _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[9])] ??
|
||||
SortOrder.desc,
|
||||
startDate: reader.readDateTimeOrNull(offsets[10]),
|
||||
description: reader.readStringOrNull(offsets[2]),
|
||||
endDate: reader.readDateTimeOrNull(offsets[3]),
|
||||
lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[4]),
|
||||
localId: reader.readStringOrNull(offsets[5]),
|
||||
modifiedAt: reader.readDateTime(offsets[6]),
|
||||
name: reader.readString(offsets[7]),
|
||||
remoteId: reader.readStringOrNull(offsets[8]),
|
||||
shared: reader.readBool(offsets[9]),
|
||||
sortOrder:
|
||||
_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ??
|
||||
SortOrder.desc,
|
||||
startDate: reader.readDateTimeOrNull(offsets[11]),
|
||||
);
|
||||
object.id = id;
|
||||
return object;
|
||||
@ -217,23 +231,25 @@ P _albumDeserializeProp<P>(
|
||||
case 1:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 2:
|
||||
return (reader.readDateTimeOrNull(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readDateTimeOrNull(offset)) as P;
|
||||
case 4:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
return (reader.readDateTimeOrNull(offset)) as P;
|
||||
case 5:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 6:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 7:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 6:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
case 7:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readBool(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 10:
|
||||
return (_AlbumsortOrderValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
SortOrder.desc) as P;
|
||||
case 10:
|
||||
case 11:
|
||||
return (reader.readDateTimeOrNull(offset)) as P;
|
||||
default:
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'endDate');
|
||||
|
@ -3,18 +3,23 @@ import 'dart:convert';
|
||||
class AlbumViewerPageState {
|
||||
final bool isEditAlbum;
|
||||
final String editTitleText;
|
||||
final String editDescriptionText;
|
||||
|
||||
AlbumViewerPageState({
|
||||
required this.isEditAlbum,
|
||||
required this.editTitleText,
|
||||
required this.editDescriptionText,
|
||||
});
|
||||
|
||||
AlbumViewerPageState copyWith({
|
||||
bool? isEditAlbum,
|
||||
String? editTitleText,
|
||||
String? editDescriptionText,
|
||||
}) {
|
||||
return AlbumViewerPageState(
|
||||
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
|
||||
editTitleText: editTitleText ?? this.editTitleText,
|
||||
editDescriptionText: editDescriptionText ?? this.editDescriptionText,
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,6 +28,7 @@ class AlbumViewerPageState {
|
||||
|
||||
result.addAll({'isEditAlbum': isEditAlbum});
|
||||
result.addAll({'editTitleText': editTitleText});
|
||||
result.addAll({'editDescriptionText': editDescriptionText});
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -31,6 +37,7 @@ class AlbumViewerPageState {
|
||||
return AlbumViewerPageState(
|
||||
isEditAlbum: map['isEditAlbum'] ?? false,
|
||||
editTitleText: map['editTitleText'] ?? '',
|
||||
editDescriptionText: map['editDescriptionText'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@ -41,7 +48,7 @@ class AlbumViewerPageState {
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
|
||||
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText, editDescriptionText: $editDescriptionText)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
@ -49,9 +56,13 @@ class AlbumViewerPageState {
|
||||
|
||||
return other is AlbumViewerPageState &&
|
||||
other.isEditAlbum == isEditAlbum &&
|
||||
other.editTitleText == editTitleText;
|
||||
other.editTitleText == editTitleText &&
|
||||
other.editDescriptionText == editDescriptionText;
|
||||
}
|
||||
|
||||
@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(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16),
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
height: 36,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
|
@ -30,15 +30,12 @@ class AlbumDateRange extends ConsumerWidget {
|
||||
final (startDate, endDate, shared) = data;
|
||||
|
||||
return Padding(
|
||||
padding: shared
|
||||
? const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
bottom: 0.0,
|
||||
)
|
||||
: const EdgeInsets.only(left: 16.0, bottom: 8.0),
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
_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(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: ((context, index) {
|
||||
return Padding(
|
||||
|
@ -19,7 +19,11 @@ class AlbumTitle extends ConsumerWidget {
|
||||
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(
|
||||
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/pages/album/album_control_button.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_title.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
@ -36,6 +37,7 @@ class AlbumViewer extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
final titleFocusNode = useFocusNode();
|
||||
final descriptionFocusNode = useFocusNode();
|
||||
final userId = ref.watch(authProvider).userId;
|
||||
final isMultiselecting = ref.watch(multiselectProvider);
|
||||
final isProcessing = useProcessingOverlay();
|
||||
@ -106,23 +108,44 @@ class AlbumViewer extends HookConsumerWidget {
|
||||
MultiselectGrid(
|
||||
key: const ValueKey("albumViewerMultiselectGrid"),
|
||||
renderListProvider: albumTimelineProvider(album.id),
|
||||
topWidget: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AlbumTitle(
|
||||
key: const ValueKey("albumTitle"),
|
||||
titleFocusNode: titleFocusNode,
|
||||
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],
|
||||
),
|
||||
const AlbumDateRange(),
|
||||
const AlbumSharedUserIcons(),
|
||||
if (album.isRemote)
|
||||
AlbumControlButton(
|
||||
key: const ValueKey("albumControlButton"),
|
||||
onAddPhotosPressed: onAddPhotosPressed,
|
||||
onAddUsersPressed: onAddUsersPressed,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
const AlbumDateRange(),
|
||||
AlbumTitle(
|
||||
key: const ValueKey("albumTitle"),
|
||||
titleFocusNode: titleFocusNode,
|
||||
),
|
||||
],
|
||||
AlbumDescription(
|
||||
key: const ValueKey("albumDescription"),
|
||||
descriptionFocusNode: descriptionFocusNode,
|
||||
),
|
||||
const AlbumSharedUserIcons(),
|
||||
if (album.isRemote)
|
||||
AlbumControlButton(
|
||||
key: const ValueKey("albumControlButton"),
|
||||
onAddPhotosPressed: onAddPhotosPressed,
|
||||
onAddUsersPressed: onAddUsersPressed,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
onRemoveFromAlbum: onRemoveFromAlbumPressed,
|
||||
editEnabled: album.ownerId == userId,
|
||||
@ -136,6 +159,7 @@ class AlbumViewer extends HookConsumerWidget {
|
||||
child: AlbumViewerAppbar(
|
||||
key: const ValueKey("albumViewerAppbar"),
|
||||
titleFocusNode: titleFocusNode,
|
||||
descriptionFocusNode: descriptionFocusNode,
|
||||
userId: userId,
|
||||
onAddPhotos: onAddPhotosPressed,
|
||||
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/providers/album/album.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/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_viewer_editable_description.dart';
|
||||
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
||||
|
||||
@RoutePage()
|
||||
@ -28,6 +30,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
final albumTitleController =
|
||||
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||
final albumTitleTextFieldFocusNode = useFocusNode();
|
||||
final albumDescriptionTextFieldFocusNode = useFocusNode();
|
||||
final isAlbumTitleTextFieldFocus = useState(false);
|
||||
final isAlbumTitleEmpty = useState(true);
|
||||
final selectedAssets = useState<Set<Asset>>(
|
||||
@ -36,6 +39,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
|
||||
void onBackgroundTapped() {
|
||||
albumTitleTextFieldFocusNode.unfocus();
|
||||
albumDescriptionTextFieldFocusNode.unfocus();
|
||||
isAlbumTitleTextFieldFocus.value = false;
|
||||
|
||||
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() {
|
||||
if (selectedAssets.value.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
@ -178,18 +195,18 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
return const SliverToBoxAdapter();
|
||||
}
|
||||
|
||||
createNonSharedAlbum() async {
|
||||
Future<void> createAlbum() async {
|
||||
onBackgroundTapped();
|
||||
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
|
||||
ref.watch(albumTitleProvider),
|
||||
ref.read(albumTitleProvider),
|
||||
selectedAssets.value,
|
||||
);
|
||||
|
||||
if (newAlbum != null) {
|
||||
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||
ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
selectedAssets.value = {};
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
|
||||
ref.read(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
ref.read(albumViewerProvider.notifier).disableEditAlbum();
|
||||
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
|
||||
}
|
||||
}
|
||||
@ -211,9 +228,8 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
).tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: albumTitleController.text.isNotEmpty
|
||||
? createNonSharedAlbum
|
||||
: null,
|
||||
onPressed:
|
||||
albumTitleController.text.isNotEmpty ? createAlbum : null,
|
||||
child: Text(
|
||||
'create'.tr(),
|
||||
style: TextStyle(
|
||||
@ -237,10 +253,11 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
pinned: true,
|
||||
floating: false,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(96.0),
|
||||
preferredSize: const Size.fromHeight(125.0),
|
||||
child: Column(
|
||||
children: [
|
||||
buildTitleInputField(),
|
||||
buildDescriptionInputField(),
|
||||
if (selectedAssets.value.isNotEmpty) buildControlButton(),
|
||||
],
|
||||
),
|
||||
|
@ -5,7 +5,13 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
AlbumViewerNotifier(this.ref)
|
||||
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
|
||||
: super(
|
||||
AlbumViewerPageState(
|
||||
editTitleText: "",
|
||||
isEditAlbum: false,
|
||||
editDescriptionText: "",
|
||||
),
|
||||
);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@ -21,12 +27,24 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
state = state.copyWith(editTitleText: newTitle);
|
||||
}
|
||||
|
||||
void setEditDescriptionText(String newDescription) {
|
||||
state = state.copyWith(editDescriptionText: newDescription);
|
||||
}
|
||||
|
||||
void remoteEditTitleText() {
|
||||
state = state.copyWith(editTitleText: "");
|
||||
}
|
||||
|
||||
void remoteEditDescriptionText() {
|
||||
state = state.copyWith(editDescriptionText: "");
|
||||
}
|
||||
|
||||
void resetState() {
|
||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||
state = state.copyWith(
|
||||
editTitleText: "",
|
||||
isEditAlbum: false,
|
||||
editDescriptionText: "",
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> changeAlbumTitle(
|
||||
@ -46,6 +64,28 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
state = state.copyWith(editTitleText: "", isEditAlbum: 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 =
|
||||
|
@ -36,6 +36,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
String name, {
|
||||
required Iterable<String> assetIds,
|
||||
Iterable<String> sharedUserIds = const [],
|
||||
String? description,
|
||||
}) async {
|
||||
final users = sharedUserIds.map(
|
||||
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
||||
@ -44,6 +45,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
_api.createAlbum(
|
||||
CreateAlbumDto(
|
||||
albumName: name,
|
||||
description: description,
|
||||
assetIds: assetIds.toList(),
|
||||
albumUsers: users.toList(),
|
||||
),
|
||||
@ -161,6 +163,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||
shared: dto.shared,
|
||||
startDate: dto.startDate,
|
||||
description: dto.description,
|
||||
endDate: dto.endDate,
|
||||
activityEnabled: dto.isActivityEnabled,
|
||||
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));
|
||||
final assets = dto.assets.map(Asset.remote).toList();
|
||||
album.assets.addAll(assets);
|
||||
|
||||
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(
|
||||
String name, {
|
||||
bool? remote,
|
||||
|
@ -451,6 +451,7 @@ class SyncService {
|
||||
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
|
||||
|
||||
album.name = dto.name;
|
||||
album.description = dto.description;
|
||||
album.shared = dto.shared;
|
||||
album.createdAt = dto.createdAt;
|
||||
album.modifiedAt = dto.modifiedAt;
|
||||
@ -643,6 +644,7 @@ class SyncService {
|
||||
toUpdate.isEmpty &&
|
||||
toDelete.isEmpty &&
|
||||
dbAlbum.name == deviceAlbum.name &&
|
||||
dbAlbum.description == deviceAlbum.description &&
|
||||
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
||||
// changes only affeted excluded albums
|
||||
_log.info(
|
||||
@ -670,6 +672,7 @@ class SyncService {
|
||||
deleteCandidates.addAll(toDelete);
|
||||
existing.addAll(existingInDb);
|
||||
dbAlbum.name = deviceAlbum.name;
|
||||
dbAlbum.description = deviceAlbum.description;
|
||||
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||
if (dbAlbum.thumbnail.value != null &&
|
||||
toDelete.contains(dbAlbum.thumbnail.value)) {
|
||||
@ -943,6 +946,7 @@ class SyncService {
|
||||
Album dbAlbum,
|
||||
) async {
|
||||
return deviceAlbum.name != dbAlbum.name ||
|
||||
deviceAlbum.description != dbAlbum.description ||
|
||||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
@ -1101,6 +1105,7 @@ class SyncService {
|
||||
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
||||
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
|
||||
remoteAlbum.name != dbAlbum.name ||
|
||||
remoteAlbum.description != dbAlbum.description ||
|
||||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
|
||||
remoteAlbum.shared != dbAlbum.shared ||
|
||||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
|
||||
|
@ -16,7 +16,7 @@ class AlbumActionFilledButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||
@ -32,9 +32,7 @@ class AlbumActionFilledButton extends StatelessWidget {
|
||||
),
|
||||
label: Text(
|
||||
labelText,
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: context.textTheme.labelLarge?.copyWith(),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
|
@ -18,6 +18,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.titleFocusNode,
|
||||
required this.descriptionFocusNode,
|
||||
this.onAddPhotos,
|
||||
this.onAddUsers,
|
||||
required this.onActivities,
|
||||
@ -25,6 +26,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
|
||||
final String userId;
|
||||
final FocusNode titleFocusNode;
|
||||
final FocusNode descriptionFocusNode;
|
||||
final void Function()? onAddPhotos;
|
||||
final void Function()? onAddUsers;
|
||||
final void Function() onActivities;
|
||||
@ -48,6 +50,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
|
||||
final albumViewer = ref.watch(albumViewerProvider);
|
||||
final newAlbumTitle = albumViewer.editTitleText;
|
||||
final newAlbumDescription = albumViewer.editDescriptionText;
|
||||
final isEditAlbum = albumViewer.isEditAlbum;
|
||||
|
||||
final comments = album.shared
|
||||
@ -277,20 +280,37 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
if (isEditAlbum) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
bool isSuccess = await ref
|
||||
.watch(albumViewerProvider.notifier)
|
||||
.changeAlbumTitle(album, newAlbumTitle);
|
||||
|
||||
if (!isSuccess) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "album_viewer_appbar_share_err_title".tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
if (newAlbumTitle.isNotEmpty) {
|
||||
bool isSuccess = await ref
|
||||
.watch(albumViewerProvider.notifier)
|
||||
.changeAlbumTitle(album, newAlbumTitle);
|
||||
if (!isSuccess) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "album_viewer_appbar_share_err_title".tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
titleFocusNode.unfocus();
|
||||
},
|
||||
icon: const Icon(Icons.check_rounded),
|
||||
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,
|
||||
style: context.textTheme.headlineMedium,
|
||||
style: context.textTheme.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
controller: titleTextEditController,
|
||||
onTap: () {
|
||||
context.focusScope.requestFocus(titleFocusNode);
|
||||
@ -65,8 +67,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 0,
|
||||
),
|
||||
suffixIcon: titleFocusNode.hasFocus
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user