diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 175db964..83fbebb9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -520,6 +520,12 @@ "@albumImporterProgressText": { "description": "Message shown while importing" }, + "editAlbumMenuLabel": "Edit album", + "@editAlbumMenuLabel": { + "description": "Edit the opened album" + }, + "doneButtonTooltip": "Done", + "changelogTitle": "Changelog", "@changelogTitle": { "description": "Title of the changelog dialog" diff --git a/lib/widget/album_viewer.dart b/lib/widget/album_viewer.dart index 961c0a0f..e7802cc0 100644 --- a/lib/widget/album_viewer.dart +++ b/lib/widget/album_viewer.dart @@ -68,11 +68,48 @@ class _AlbumViewerState extends State build(BuildContext context) { return AppTheme( child: Scaffold( - body: Builder(builder: (context) => _buildContent(context)), + body: Builder( + builder: (context) { + if (isEditMode) { + return Form( + key: _editFormKey, + child: _buildContent(context), + ); + } else { + return _buildContent(context); + } + }, + ), ), ); } + @override + doneEditMode() { + if (_editFormKey?.currentState?.validate() == true) { + _editFormKey.currentState.save(); + final newAlbum = makeEdited(_album); + if (newAlbum.copyWith(lastUpdated: _album.lastUpdated) != _album) { + _log.info("[doneEditMode] Album modified: $newAlbum"); + final albumRepo = AlbumRepo(AlbumCachedDataSource()); + setState(() { + _album = newAlbum; + }); + UpdateAlbum(albumRepo)(widget.account, newAlbum) + .catchError((e, stacktrace) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e, context)), + duration: k.snackBarDurationNormal, + )); + }); + } else { + _log.fine("[doneEditMode] Album not modified"); + } + return true; + } + return false; + } + void _initAlbum() { assert(widget.album.provider is AlbumStaticProvider); ResyncAlbum()(widget.account, widget.album).then((album) { @@ -114,7 +151,13 @@ class _AlbumViewerState extends State _buildAppBar(context), SliverPadding( padding: const EdgeInsets.all(16), - sliver: buildItemStreamList(context), + sliver: SliverIgnorePointer( + ignoring: isEditMode, + sliver: SliverOpacity( + opacity: isEditMode ? .25 : 1, + sliver: buildItemStreamList(context), + ), + ), ), ], ), @@ -123,21 +166,31 @@ class _AlbumViewerState extends State } Widget _buildAppBar(BuildContext context) { - if (isSelectionMode) { - return buildSelectionAppBar(context, [ - IconButton( - icon: const Icon(Icons.remove), - tooltip: AppLocalizations.of(context).removeSelectedFromAlbumTooltip, - onPressed: () { - _onSelectionAppBarRemovePressed(); - }, - ) - ]); + if (isEditMode) { + return _buildEditAppBar(context); + } else if (isSelectionMode) { + return _buildSelectionAppBar(context); } else { return buildNormalAppBar(context, widget.account, _album); } } + Widget _buildSelectionAppBar(BuildContext context) { + return buildSelectionAppBar(context, [ + IconButton( + icon: const Icon(Icons.remove), + tooltip: AppLocalizations.of(context).removeSelectedFromAlbumTooltip, + onPressed: () { + _onSelectionAppBarRemovePressed(); + }, + ) + ]); + } + + Widget _buildEditAppBar(BuildContext context) { + return buildEditAppBar(context, widget.account, widget.album); + } + void _onItemTap(int index) { Navigator.pushNamed(context, Viewer.routeName, arguments: ViewerArguments(widget.account, _backingFiles, index)); @@ -257,6 +310,8 @@ class _AlbumViewerState extends State Album _album; var _backingFiles = []; + final _editFormKey = GlobalKey(); + static final _log = Logger("widget.album_viewer._AlbumViewerState"); } diff --git a/lib/widget/album_viewer_mixin.dart b/lib/widget/album_viewer_mixin.dart index cbef1448..3b900df0 100644 --- a/lib/widget/album_viewer_mixin.dart +++ b/lib/widget/album_viewer_mixin.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api_util.dart' as api_util; @@ -39,38 +40,14 @@ mixin AlbumViewerMixin Account account, Album album, { List actions, + List> Function(BuildContext) menuItemBuilder, + void Function(int) onSelectedMenuItem, }) { - Widget cover; - try { - if (_coverPreviewUrl != null) { - cover = Opacity( - opacity: - Theme.of(context).brightness == Brightness.light ? 0.25 : 0.35, - child: FittedBox( - clipBehavior: Clip.hardEdge, - fit: BoxFit.cover, - child: CachedNetworkImage( - imageUrl: _coverPreviewUrl, - httpHeaders: { - "Authorization": Api.getAuthorizationHeaderValue(account), - }, - filterQuality: FilterQuality.high, - errorWidget: (context, url, error) { - // just leave it empty - return Container(); - }, - imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, - ), - ), - ); - } - } catch (_) {} - return SliverAppBar( floating: true, expandedHeight: 160, flexibleSpace: FlexibleSpaceBar( - background: cover, + background: _getAppBarCover(context, account), title: Text( album.name, style: TextStyle( @@ -97,6 +74,31 @@ mixin AlbumViewerMixin ], ), ...(actions ?? []), + PopupMenuButton( + tooltip: MaterialLocalizations.of(context).moreButtonTooltip, + itemBuilder: (context) => [ + PopupMenuItem( + value: -1, + child: Text(AppLocalizations.of(context).editAlbumMenuLabel), + ), + ...(menuItemBuilder?.call(context) ?? []), + ], + onSelected: (option) { + if (option >= 0) { + onSelectedMenuItem?.call(option); + } else { + switch (option) { + case _menuValueEdit: + _onAppBarEditPressed(context, album); + break; + + default: + _log.shout("[buildNormalAppBar] Unknown value: $option"); + break; + } + } + }, + ), ], ); } @@ -125,9 +127,75 @@ mixin AlbumViewerMixin ); } + @protected + Widget buildEditAppBar( + BuildContext context, + Account account, + Album album, { + List actions, + }) { + return SliverAppBar( + floating: true, + expandedHeight: 160, + flexibleSpace: FlexibleSpaceBar( + background: _getAppBarCover(context, account), + title: TextFormField( + decoration: InputDecoration( + hintText: AppLocalizations.of(context).nameInputHint, + ), + validator: (value) { + if (value.isEmpty) { + return AppLocalizations.of(context).albumNameInputInvalidEmpty; + } + return null; + }, + onSaved: (value) { + _editFormValue.name = value; + }, + onChanged: (value) { + // need to save the value otherwise it'll return to the initial + // after scrolling out of the view + _editNameValue = value; + }, + style: TextStyle( + color: AppTheme.getPrimaryTextColor(context), + ), + initialValue: _editNameValue, + ), + ), + leading: IconButton( + icon: const Icon(Icons.check), + color: Theme.of(context).colorScheme.primary, + tooltip: AppLocalizations.of(context).doneButtonTooltip, + onPressed: () { + if (doneEditMode()) { + setState(() { + _isEditMode = false; + }); + } + }, + ), + actions: actions, + ); + } + @override get itemStreamListCellSize => thumbSize; + @protected + get isEditMode => _isEditMode; + + @protected + bool doneEditMode(); + + /// Return a new album with the edits + @protected + Album makeEdited(Album album) { + return album.copyWith( + name: _editFormValue.name, + ); + } + @protected int get thumbSize { switch (_thumbZoomLevel) { @@ -143,6 +211,53 @@ mixin AlbumViewerMixin } } + void _onAppBarEditPressed(BuildContext context, Album album) { + setState(() { + _isEditMode = true; + _editNameValue = album.name; + _editFormValue = _EditFormValue(); + }); + } + + Widget _getAppBarCover(BuildContext context, Account account) { + try { + if (_coverPreviewUrl != null) { + return Opacity( + opacity: + Theme.of(context).brightness == Brightness.light ? 0.25 : 0.35, + child: FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.cover, + child: CachedNetworkImage( + imageUrl: _coverPreviewUrl, + httpHeaders: { + "Authorization": Api.getAuthorizationHeaderValue(account), + }, + filterQuality: FilterQuality.high, + errorWidget: (context, url, error) { + // just leave it empty + return Container(); + }, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + ), + ), + ); + } + } catch (_) {} + return null; + } + String _coverPreviewUrl; var _thumbZoomLevel = 0; + + var _isEditMode = false; + String _editNameValue; + var _editFormValue = _EditFormValue(); + + static final _log = Logger("widget.album_viewer_mixin.AlbumViewerMixin"); + static const _menuValueEdit = -1; +} + +class _EditFormValue { + String name; } diff --git a/lib/widget/dynamic_album_viewer.dart b/lib/widget/dynamic_album_viewer.dart index 4ba07c49..372e1e8c 100644 --- a/lib/widget/dynamic_album_viewer.dart +++ b/lib/widget/dynamic_album_viewer.dart @@ -72,11 +72,48 @@ class _DynamicAlbumViewerState extends State build(BuildContext context) { return AppTheme( child: Scaffold( - body: Builder(builder: (context) => _buildContent(context)), + body: Builder( + builder: (context) { + if (isEditMode) { + return Form( + key: _editFormKey, + child: _buildContent(context), + ); + } else { + return _buildContent(context); + } + }, + ), ), ); } + @override + doneEditMode() { + if (_editFormKey?.currentState?.validate() == true) { + _editFormKey.currentState.save(); + final newAlbum = makeEdited(_album); + if (newAlbum.copyWith(lastUpdated: _album.lastUpdated) != _album) { + _log.info("[doneEditMode] Album modified: $newAlbum"); + final albumRepo = AlbumRepo(AlbumCachedDataSource()); + setState(() { + _album = newAlbum; + }); + UpdateAlbum(albumRepo)(widget.account, newAlbum) + .catchError((e, stacktrace) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e, context)), + duration: k.snackBarDurationNormal, + )); + }); + } else { + _log.fine("[doneEditMode] Album not modified"); + } + return true; + } + return false; + } + void _initAlbum() { assert(widget.album.provider is AlbumDynamicProvider); PopulateAlbum()(widget.account, widget.album).then((items) { @@ -138,7 +175,13 @@ class _DynamicAlbumViewerState extends State _buildAppBar(context), SliverPadding( padding: const EdgeInsets.all(16), - sliver: buildItemStreamList(context), + sliver: SliverIgnorePointer( + ignoring: isEditMode, + sliver: SliverOpacity( + opacity: isEditMode ? .25 : 1, + sliver: buildItemStreamList(context), + ), + ), ), ], ), @@ -146,38 +189,38 @@ class _DynamicAlbumViewerState extends State ); } - Widget _buildAppBar(BuildContext context) => isSelectionMode - ? _buildSelectionAppBar(context) - : _buildNormalAppBar(context); + Widget _buildAppBar(BuildContext context) { + if (isEditMode) { + return _buildEditAppBar(context); + } else if (isSelectionMode) { + return _buildSelectionAppBar(context); + } else { + return _buildNormalAppBar(context); + } + } Widget _buildNormalAppBar(BuildContext context) { return buildNormalAppBar( context, widget.account, _album, - actions: [ - PopupMenuButton( - tooltip: MaterialLocalizations.of(context).moreButtonTooltip, - itemBuilder: (context) => [ - PopupMenuItem( - value: _AppBarOption.convertBasic, - child: - Text(AppLocalizations.of(context).convertBasicAlbumMenuLabel), - ), - ], - onSelected: (option) { - switch (option) { - case _AppBarOption.convertBasic: - _onAppBarConvertBasicPressed(context); - break; - - default: - _log.shout("[_buildNormalAppBar] Unknown value: $option"); - break; - } - }, + menuItemBuilder: (context) => [ + PopupMenuItem( + value: _menuValueConvertBasic, + child: Text(AppLocalizations.of(context).convertBasicAlbumMenuLabel), ), ], + onSelectedMenuItem: (option) { + switch (option) { + case _menuValueConvertBasic: + _onAppBarConvertBasicPressed(context); + break; + + default: + _log.shout("[_buildNormalAppBar] Unknown value: $option"); + break; + } + }, ); } @@ -200,6 +243,10 @@ class _DynamicAlbumViewerState extends State ]); } + Widget _buildEditAppBar(BuildContext context) { + return buildEditAppBar(context, widget.account, widget.album); + } + void _onItemTap(int index) { Navigator.pushNamed(context, Viewer.routeName, arguments: ViewerArguments(widget.account, _backingFiles, index)); @@ -358,8 +405,11 @@ class _DynamicAlbumViewerState extends State List _items; var _backingFiles = []; + final _editFormKey = GlobalKey(); + static final _log = Logger("widget.dynamic_album_viewer._DynamicAlbumViewerState"); + static const _menuValueConvertBasic = 0; } class _ImageListItem extends SelectableItemStreamListItem { @@ -403,10 +453,6 @@ class _VideoListItem extends SelectableItemStreamListItem { final String previewUrl; } -enum _AppBarOption { - convertBasic, -} - enum _SelectionAppBarOption { delete, }