shortcut: select icon image

This commit is contained in:
Thibault Deckers 2021-03-24 16:23:50 +09:00
parent 562f3057ed
commit bbe1f496d2
3 changed files with 125 additions and 36 deletions

View file

@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
### Added
- Collection / Albums / Countries / Tags: added label when dragging scrollbar thumb
- Albums: localized common album names
- Collection: select shortcut icon image
### Changed
- Upgraded Flutter to beta v2.1.0-12.2.pre

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/collection_actions.dart';
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
@ -27,6 +28,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:pedantic/pedantic.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class CollectionAppBar extends StatefulWidget {
final ValueNotifier<double> appBarHeightNotifier;
@ -347,22 +349,25 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Future<void> _showShortcutDialog(BuildContext context) async {
final filters = collection.filters;
var defaultName;
if (filters.isEmpty) {
defaultName = context.l10n.collectionPageTitle;
} else {
if (filters.isNotEmpty) {
// we compute the default name beforehand
// because some filter labels need localization
final sortedFilters = List<CollectionFilter>.from(filters)..sort();
defaultName = sortedFilters.first.getLabel(context);
}
final name = await showDialog<String>(
final result = await showDialog<Tuple2<AvesEntry, String>>(
context: context,
builder: (context) {
return AddShortcutDialog(defaultName: defaultName);
},
builder: (context) => AddShortcutDialog(
collection: collection,
defaultName: defaultName,
),
);
final coverEntry = result.item1;
final name = result.item2;
if (name == null || name.isEmpty) return;
final iconEntry = collection.sortedEntries.isNotEmpty ? collection.sortedEntries.first : null;
unawaited(AppShortcutService.pin(name, iconEntry, filters));
unawaited(AppShortcutService.pin(name, coverEntry, collection.filters));
}
void _goToSearch() {

View file

@ -1,12 +1,24 @@
import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/collection/thumbnail/raster.dart';
import 'package:aves/widgets/collection/thumbnail/vector.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'aves_dialog.dart';
class AddShortcutDialog extends StatefulWidget {
final CollectionLens collection;
final String defaultName;
const AddShortcutDialog({
@required this.collection,
@required this.defaultName,
});
@ -17,10 +29,20 @@ class AddShortcutDialog extends StatefulWidget {
class _AddShortcutDialogState extends State<AddShortcutDialog> {
final TextEditingController _nameController = TextEditingController();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
AvesEntry _coverEntry;
CollectionLens get collection => widget.collection;
Set<CollectionFilter> get filters => collection.filters;
@override
void initState() {
super.initState();
final entries = collection.sortedEntries;
if (entries.isNotEmpty) {
final coverEntries = filters.map(covers.coverContentId).where((id) => id != null).map((id) => entries.firstWhere((entry) => entry.contentId == id, orElse: () => null)).where((entry) => entry != null);
_coverEntry = coverEntries.isNotEmpty ? coverEntries.first : entries.first;
}
_nameController.text = widget.defaultName;
_validate();
}
@ -33,40 +55,101 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
@override
Widget build(BuildContext context) {
return AvesDialog(
context: context,
content: TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: context.l10n.addShortcutDialogLabel,
),
autofocus: true,
maxLength: 25,
onChanged: (_) => _validate(),
onSubmitted: (_) => _submit(context),
return MediaQueryDataProvider(
child: Builder(
builder: (context) {
final shortestSide = context.select<MediaQueryData, double>((mq) => mq.size.shortestSide);
final extent = (shortestSide / 3.0).clamp(60.0, 160.0);
return AvesDialog(
context: context,
scrollableContent: [
if (_coverEntry != null)
Container(
alignment: Alignment.center,
padding: EdgeInsets.only(top: 16),
child: _buildCover(_coverEntry, extent),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 24),
child: TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: context.l10n.addShortcutDialogLabel,
),
autofocus: true,
maxLength: 25,
onChanged: (_) => _validate(),
onSubmitted: (_) => _submit(context),
),
),
],
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return TextButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text(context.l10n.addShortcutButtonLabel),
);
},
)
],
);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return TextButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text(context.l10n.addShortcutButtonLabel),
);
},
)
],
);
}
Widget _buildCover(AvesEntry entry, double extent) {
return GestureDetector(
onTap: _pickEntry,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(32)),
child: SizedBox(
width: extent,
height: extent,
child: entry.isSvg
? VectorImageThumbnail(
entry: entry,
extent: extent,
)
: RasterImageThumbnail(
entry: entry,
extent: extent,
),
),
),
);
}
Future<void> _pickEntry() async {
final entry = await Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: ItemPickDialog.routeName),
builder: (context) => ItemPickDialog(
CollectionLens(
source: collection.source,
filters: filters,
),
),
fullscreenDialog: true,
),
);
if (entry != null) {
_coverEntry = entry;
setState(() {});
}
}
Future<void> _validate() async {
final name = _nameController.text ?? '';
_isValidNotifier.value = name.isNotEmpty;
}
void _submit(BuildContext context) => Navigator.pop(context, _nameController.text);
void _submit(BuildContext context) => Navigator.pop(context, Tuple2<AvesEntry, String>(_coverEntry, _nameController.text));
}