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

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