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:
JobiJoba 2025-06-05 00:41:28 +07:00 committed by GitHub
parent 19ff39c2b9
commit 5d0ad853f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 598 additions and 98 deletions

View File

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

View File

@ -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.desc, sortOrder:
startDate: reader.readDateTimeOrNull(offsets[10]), _AlbumsortOrderValueEnumMap[reader.readByteOrNull(offsets[10])] ??
SortOrder.desc,
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');

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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,23 +108,44 @@ 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(
mainAxisAlignment: MainAxisAlignment.end, decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.start, gradient: LinearGradient(
children: [ begin: Alignment.topCenter,
AlbumTitle( end: Alignment.bottomCenter,
key: const ValueKey("albumTitle"), colors: [
titleFocusNode: titleFocusNode, 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(), child: Column(
if (album.isRemote) mainAxisAlignment: MainAxisAlignment.end,
AlbumControlButton( crossAxisAlignment: CrossAxisAlignment.start,
key: const ValueKey("albumControlButton"), children: [
onAddPhotosPressed: onAddPhotosPressed, const SizedBox(height: 32),
onAddUsersPressed: onAddUsersPressed, 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, 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,20 +280,37 @@ class AlbumViewerAppbar extends HookConsumerWidget
if (isEditAlbum) { if (isEditAlbum) {
return IconButton( return IconButton(
onPressed: () async { onPressed: () async {
bool isSuccess = await ref if (newAlbumTitle.isNotEmpty) {
.watch(albumViewerProvider.notifier) bool isSuccess = await ref
.changeAlbumTitle(album, newAlbumTitle); .watch(albumViewerProvider.notifier)
.changeAlbumTitle(album, newAlbumTitle);
if (!isSuccess) { if (!isSuccess) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: "album_viewer_appbar_share_err_title".tr(), msg: "album_viewer_appbar_share_err_title".tr(),
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
toastType: ToastType.error, 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), icon: const Icon(Icons.check_rounded),
splashRadius: 25, splashRadius: 25,

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

View File

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