mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-27 17:34:44 +01:00
Create folder based album
This commit is contained in:
parent
3b3ce66c3b
commit
b04803e3bd
6 changed files with 333 additions and 27 deletions
|
@ -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"
|
||||
|
|
130
lib/widget/album_dir_picker.dart
Normal file
130
lib/widget/album_dir_picker.dart
Normal 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");
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue