Create folder based album

This commit is contained in:
Ming Ming 2021-06-29 17:44:35 +08:00
parent 3b3ce66c3b
commit b04803e3bd
6 changed files with 333 additions and 27 deletions

View file

@ -459,6 +459,34 @@
"@updateDateTimeFailureNotification": {
"description": "Failed to set the date & time of a file"
},
"albumDirPickerHeaderText": "Pick the folders to be associated",
"@albumDirPickerHeaderText": {
"description": "Pick the folders for a folder based album"
},
"albumDirPickerSubHeaderText": "Only photos in the associated folders will be included in this album",
"@albumDirPickerSubHeaderText": {
"description": "Pick the folders for a folder based album"
},
"albumDirPickerListEmptyNotification": "Please pick at least one folder",
"@albumDirPickerListEmptyNotification": {
"description": "Error when user pressing confirm without picking any folders"
},
"createAlbumDialogBasicLabel": "Basic",
"@createAlbumDialogBasicLabel": {
"description": "Basic album"
},
"createAlbumDialogBasicDescription": "Basic album organizes photos regardless of the file hierarchy on the server",
"@createAlbumDialogBasicDescription": {
"description": "Describe what a basic album is"
},
"createAlbumDialogFolderBasedLabel": "Folder based",
"@createAlbumDialogFolderBasedLabel": {
"description": "Folder based album"
},
"createAlbumDialogFolderBasedDescription": "Folder based album reflects contents of a folder",
"@createAlbumDialogFolderBasedDescription": {
"description": "Describe what a folder based album is"
},
"changelogTitle": "Changelog",
"@changelogTitle": {
"description": "Title of the changelog dialog"

View file

@ -0,0 +1,130 @@
import 'package:flutter/foundation.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_util.dart' as api_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/dir_picker_mixin.dart';
class AlbumDirPickerArguments {
AlbumDirPickerArguments(this.account);
final Account account;
}
class AlbumDirPicker extends StatefulWidget {
static const routeName = "/album-dir-picker";
AlbumDirPicker({
Key key,
@required this.account,
}) : super(key: key);
AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key key})
: this(
key: key,
account: args.account,
);
@override
createState() => _AlbumDirPickerState();
final Account account;
}
class _AlbumDirPickerState extends State<AlbumDirPicker>
with DirPickerMixin<AlbumDirPicker> {
@override
build(BuildContext context) {
return AppTheme(
child: Scaffold(
body: _buildContent(context),
),
);
}
@override
getPickerRoot() {
var root = api_util.getWebdavRootUrlRelative(widget.account);
if (widget.account.roots.length == 1) {
return "$root/${widget.account.roots.first}";
} else {
return root;
}
}
@override
getAccount() => widget.account;
Widget _buildContent(BuildContext context) {
return SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
AppLocalizations.of(context).albumDirPickerHeaderText,
style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Align(
alignment: AlignmentDirectional.topStart,
child: Text(
AppLocalizations.of(context).albumDirPickerSubHeaderText,
),
),
],
),
),
Expanded(
child: buildDirPicker(context),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child:
Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ElevatedButton(
onPressed: () => _onConfirmPressed(context),
child: Text(AppLocalizations.of(context).confirmButtonLabel),
),
],
),
),
],
),
);
}
void _onConfirmPressed(BuildContext context) {
final picked = getPickedDirs();
if (picked.isEmpty) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(
AppLocalizations.of(context).albumDirPickerListEmptyNotification),
duration: k.snackBarDurationNormal,
));
} else {
_log.info(
"[_onConfirmPressed] Picked: ${picked.map((e) => e.strippedPath).toReadableString()}");
Navigator.of(context).pop(picked);
}
}
static final _log = Logger("widget.album_dir_picker._AlbumDirPickerState");
}

View file

@ -128,6 +128,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
context: context,
builder: (_) => NewAlbumDialog(
account: widget.account,
isAllowDynamic: false,
),
).then((value) {
Navigator.of(context).pop(value);

View file

@ -337,7 +337,17 @@ class _HomeAlbumsState extends State<HomeAlbums> {
builder: (_) => NewAlbumDialog(
account: widget.account,
),
).catchError((e, stacktrace) {
).then((album) {
if (album == null || album is! Album) {
return;
}
if (album.provider is AlbumDynamicProvider) {
// open the album automatically to refresh its content, otherwise it'll
// be empty
Navigator.of(context).pushNamed(DynamicAlbumViewer.routeName,
arguments: DynamicAlbumViewerArguments(widget.account, album));
}
}).catchError((e, stacktrace) {
_log.severe(
"[_onNewAlbumItemTap] Failed while showDialog", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar(

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/language_util.dart' as language_util;
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/album_dir_picker.dart';
import 'package:nc_photos/widget/album_viewer.dart';
import 'package:nc_photos/widget/archive_viewer.dart';
import 'package:nc_photos/widget/connect.dart';
@ -92,6 +93,7 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
route ??= _handleSettingsRoute(settings);
route ??= _handleArchiveViewerRoute(settings);
route ??= _handleDynamicAlbumViewerRoute(settings);
route ??= _handleAlbumDirPickerRoute(settings);
return route;
}
@ -230,6 +232,22 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null;
}
Route<dynamic> _handleAlbumDirPickerRoute(RouteSettings settings) {
try {
if (settings.name == AlbumDirPicker.routeName &&
settings.arguments != null) {
final AlbumDirPickerArguments args = settings.arguments;
return MaterialPageRoute(
builder: (context) => AlbumDirPicker.fromArgs(args),
);
}
} catch (e) {
_log.severe(
"[_handleAlbumDirPickerRoute] Failed while handling route", e);
}
return null;
}
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
AppEventListener<ThemeChangedEvent> _themeChangedListener;

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/use_case/create_album.dart';
import 'package:nc_photos/widget/album_dir_picker.dart';
/// Dialog to create a new album
///
@ -16,12 +17,14 @@ class NewAlbumDialog extends StatefulWidget {
NewAlbumDialog({
Key key,
@required this.account,
this.isAllowDynamic = true,
}) : super(key: key);
@override
createState() => _NewAlbumDialogState();
final Account account;
final bool isAllowDynamic;
}
class _NewAlbumDialogState extends State<NewAlbumDialog> {
@ -32,59 +35,175 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
@override
build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).createAlbumTooltip),
content: Form(
key: _formKey,
child: TextFormField(
decoration: InputDecoration(
hintText: AppLocalizations.of(context).nameInputHint,
return Visibility(
visible: _isVisible,
child: AlertDialog(
title: Text(AppLocalizations.of(context).createAlbumTooltip),
content: Form(
key: _formKey,
child: Container(
constraints: BoxConstraints.tightFor(width: 280),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
hintText: AppLocalizations.of(context).nameInputHint,
),
validator: (value) {
if (value.isEmpty) {
return AppLocalizations.of(context)
.albumNameInputInvalidEmpty;
}
return null;
},
onSaved: (value) {
_formValue.name = value;
},
),
if (widget.isAllowDynamic) ...[
DropdownButtonHideUnderline(
child: DropdownButtonFormField<_Provider>(
value: _provider,
items: [_Provider.static, _Provider.dir]
.map((e) => DropdownMenuItem<_Provider>(
value: e,
child: Text(e.toValueString(context)),
))
.toList(),
onChanged: (newValue) {
setState(() {
_provider = newValue;
});
},
onSaved: (value) {
_formValue.provider = value;
},
),
),
const SizedBox(height: 8),
Text(
_provider.toDescription(context),
style: Theme.of(context).textTheme.bodyText2,
),
],
],
),
),
validator: (value) {
if (value.isEmpty) {
return AppLocalizations.of(context).albumNameInputInvalidEmpty;
}
return null;
},
onSaved: (value) {
_formValue.name = value;
},
),
actions: [
TextButton(
onPressed: () => _onOkPressed(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
actions: [
TextButton(
onPressed: () => _onOkPressed(context),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
);
}
void _onOkPressed(BuildContext context) {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
if (_formValue.provider == _Provider.static) {
_onConfirmStaticAlbum();
} else {
_onConfirmDirAlbum();
}
}
}
void _onConfirmStaticAlbum() {
final album = Album(
name: _formValue.name,
provider: AlbumStaticProvider(
items: const [],
),
coverProvider: AlbumAutoCoverProvider(),
);
_log.info("[_onOkPressed] Creating static album: $album");
final albumRepo = AlbumRepo(AlbumCachedDataSource());
final newAlbum = CreateAlbum(albumRepo)(widget.account, album);
// let previous route to handle this future
Navigator.of(context).pop(newAlbum);
}
void _onConfirmDirAlbum() {
setState(() {
_isVisible = false;
});
Navigator.of(context)
.pushNamed(AlbumDirPicker.routeName,
arguments: AlbumDirPickerArguments(widget.account))
.then((value) {
if (value == null) {
Navigator.of(context).pop();
return;
}
final album = Album(
name: _formValue.name,
provider: AlbumStaticProvider(
items: const [],
provider: AlbumDirProvider(
dirs: value,
),
coverProvider: AlbumAutoCoverProvider(),
);
_log.info("[_onOkPressed] Creating album: $album");
_log.info("[_onOkPressed] Creating dir album: $album");
final albumRepo = AlbumRepo(AlbumCachedDataSource());
final newAlbum = CreateAlbum(albumRepo)(widget.account, album);
// let previous route to handle this future
Navigator.of(context).pop(newAlbum);
}
}).catchError((e, stacktrace) {
_log.shout("[_onOkPressed] Failed while pushNamed", e, stacktrace);
Navigator.of(context).pop();
});
}
final _formKey = GlobalKey<FormState>();
var _provider = _Provider.static;
final _formValue = _FormValue();
var _isVisible = true;
static final _log = Logger("widget.new_album_dialog._AlbumPickerDialogState");
}
class _FormValue {
String name;
_Provider provider;
}
enum _Provider {
static,
dir,
}
extension on _Provider {
String toValueString(BuildContext context) {
switch (this) {
case _Provider.static:
return AppLocalizations.of(context).createAlbumDialogBasicLabel;
case _Provider.dir:
return AppLocalizations.of(context).createAlbumDialogFolderBasedLabel;
default:
throw StateError("Unknown value: $this");
}
}
String toDescription(BuildContext context) {
switch (this) {
case _Provider.static:
return AppLocalizations.of(context).createAlbumDialogBasicDescription;
case _Provider.dir:
return AppLocalizations.of(context)
.createAlbumDialogFolderBasedDescription;
default:
throw StateError("Unknown value: $this");
}
}
}