#437 tv: rail (wip)
This commit is contained in:
parent
053ff71949
commit
3a151638e8
51 changed files with 1964 additions and 1796 deletions
|
@ -15,8 +15,7 @@ class AboutPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.aboutPageTitle),
|
||||
),
|
||||
|
@ -48,7 +47,6 @@ class AboutPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import 'package:aves/widgets/common/behaviour/route_tracker.dart';
|
|||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/home_page.dart';
|
||||
import 'package:aves/widgets/welcome_page.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
|
@ -249,7 +250,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
data: Theme.of(context).copyWith(
|
||||
pageTransitionsTheme: pageTransitionsTheme,
|
||||
),
|
||||
child: child!,
|
||||
child: MediaQueryDataProvider(child: child!),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
|
@ -18,11 +19,11 @@ import 'package:aves/widgets/common/basic/insets.dart';
|
|||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_fab.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -80,23 +81,11 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
|
||||
return MediaQueryDataProvider(
|
||||
child: SelectionProvider<AvesEntry>(
|
||||
return SelectionProvider<AvesEntry>(
|
||||
child: Selector<Selection<AvesEntry>, bool>(
|
||||
selector: (context, selection) => selection.selectedItems.isNotEmpty,
|
||||
builder: (context, hasSelection, child) {
|
||||
return Selector<Settings, bool>(
|
||||
selector: (context, s) => s.enableBottomNavigationBar,
|
||||
builder: (context, enableBottomNavigationBar, child) {
|
||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
||||
return NotificationListener<DraggableScrollBarNotification>(
|
||||
onNotification: (notification) {
|
||||
_draggableScrollBarEventStreamController.add(notification.event);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: QueryProvider(
|
||||
final body = QueryProvider(
|
||||
initialQuery: liveFilter?.query,
|
||||
child: Builder(
|
||||
builder: (context) => WillPopScope(
|
||||
|
@ -126,7 +115,33 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (device.isTelevision) {
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
TvRail(currentCollection: _collection),
|
||||
Expanded(child: body),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
);
|
||||
} else {
|
||||
return Selector<Settings, bool>(
|
||||
selector: (context, s) => s.enableBottomNavigationBar,
|
||||
builder: (context, enableBottomNavigationBar, child) {
|
||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
||||
|
||||
return NotificationListener<DraggableScrollBarNotification>(
|
||||
onNotification: (notification) {
|
||||
_draggableScrollBarEventStreamController.add(notification.event);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: body,
|
||||
floatingActionButton: _buildFab(context, hasSelection),
|
||||
drawer: canNavigate ? AppDrawer(currentCollection: _collection) : null,
|
||||
bottomNavigationBar: showBottomNavigationBar
|
||||
|
@ -141,9 +156,9 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ import 'package:aves/widgets/common/search/route.dart';
|
|||
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/location_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_page.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
|
||||
import 'package:aves/widgets/map/map_page.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/stats/stats_page.dart';
|
||||
|
@ -517,8 +517,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
final location = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: LocationPickDialog.routeName),
|
||||
builder: (context) => LocationPickDialog(
|
||||
settings: const RouteSettings(name: LocationPickPage.routeName),
|
||||
builder: (context) => LocationPickPage(
|
||||
collection: mapCollection,
|
||||
initialLocation: clusterLocation,
|
||||
),
|
||||
|
|
|
@ -14,8 +14,8 @@ import 'package:aves/widgets/dialogs/entry_editors/edit_date_dialog.dart';
|
|||
import 'package:aves/widgets/dialogs/entry_editors/edit_description_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_location_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_rating_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/edit_tags_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/remove_metadata_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/tag_editor_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:ui';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/debouncer.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/search/delegate.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -116,8 +115,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||
case null:
|
||||
break;
|
||||
}
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: Hero(
|
||||
tag: AvesAppBar.leadingHeroTag,
|
||||
|
@ -149,7 +147,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||
duration: const Duration(milliseconds: 300),
|
||||
child: body,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/debug/android_apps.dart';
|
||||
import 'package:aves/widgets/debug/android_codecs.dart';
|
||||
import 'package:aves/widgets/debug/android_dirs.dart';
|
||||
|
@ -41,8 +40,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Directionality(
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
@ -86,7 +84,6 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import 'package:aves/model/filters/query.dart';
|
|||
import 'package:aves/model/source/collection_lens.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:aves/widgets/dialogs/item_picker.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -114,7 +114,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||
settings: const RouteSettings(name: ItemPickPage.routeName),
|
||||
builder: (context) {
|
||||
final pickFilters = _collection.filters.toSet();
|
||||
final liveFilters = pickFilters.whereType<QueryFilter>().where((v) => v.live).toSet();
|
||||
|
@ -122,7 +122,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
pickFilters.remove(filter);
|
||||
pickFilters.add(QueryFilter(filter.query));
|
||||
});
|
||||
return ItemPickDialog(
|
||||
return ItemPickPage(
|
||||
collection: CollectionLens(
|
||||
source: _collection.source,
|
||||
filters: pickFilters,
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppPickDialog extends StatefulWidget {
|
||||
static const routeName = '/app_pick';
|
||||
|
||||
final String? initialValue;
|
||||
|
||||
const AppPickDialog({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AppPickDialog> createState() => _AppPickDialogState();
|
||||
}
|
||||
|
||||
class _AppPickDialogState extends State<AppPickDialog> {
|
||||
late String? _selectedValue;
|
||||
late Future<Set<Package>> _loader;
|
||||
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||
|
||||
static const double iconSize = 32;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedValue = widget.initialValue;
|
||||
_loader = androidAppService.getPackages();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.appPickDialogTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<Set<Package>>(
|
||||
future: _loader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final allPackages = snapshot.data;
|
||||
if (allPackages == null) return const SizedBox();
|
||||
final packages = allPackages.where((package) => package.categoryLauncher).toList()..sort((a, b) => compareAsciiUpperCase(_displayName(a), _displayName(b)));
|
||||
return Column(
|
||||
children: [
|
||||
QueryBar(queryNotifier: _queryNotifier),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _queryNotifier,
|
||||
builder: (context, query, child) {
|
||||
final upQuery = query.toUpperCase().trim();
|
||||
final visiblePackages = packages.where((package) {
|
||||
return {
|
||||
package.packageName,
|
||||
package.currentLabel,
|
||||
package.englishLabel,
|
||||
...package.potentialDirs,
|
||||
}.any((v) => v != null && v.toUpperCase().contains(upQuery));
|
||||
}).toList();
|
||||
final showNoneOption = upQuery.isEmpty;
|
||||
final itemCount = visiblePackages.length + (showNoneOption ? 1 : 0);
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (showNoneOption) {
|
||||
if (index == 0) {
|
||||
return ReselectableRadioListTile<String?>(
|
||||
value: '',
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text(
|
||||
context.l10n.appPickDialogNone,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
index--;
|
||||
}
|
||||
|
||||
final package = visiblePackages[index];
|
||||
return ReselectableRadioListTile<String?>(
|
||||
value: package.packageName,
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: Image(
|
||||
image: AppIconImage(
|
||||
packageName: package.packageName,
|
||||
size: iconSize,
|
||||
),
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: _displayName(package),
|
||||
),
|
||||
],
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: itemCount,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _displayName(Package package) => package.currentLabel ?? package.packageName;
|
||||
}
|
|
@ -15,7 +15,7 @@ import 'package:aves/widgets/common/basic/wheel.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/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -314,8 +314,8 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||
builder: (context) => ItemPickDialog(
|
||||
settings: const RouteSettings(name: ItemPickPage.routeName),
|
||||
builder: (context) => ItemPickPage(
|
||||
collection: CollectionLens(
|
||||
source: _collection.source,
|
||||
),
|
||||
|
|
|
@ -13,9 +13,9 @@ import 'package:aves/widgets/common/basic/text_dropdown_button.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/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
import 'package:aves/widgets/dialogs/location_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
@ -186,8 +186,8 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
final latLng = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: LocationPickDialog.routeName),
|
||||
builder: (context) => LocationPickDialog(
|
||||
settings: const RouteSettings(name: LocationPickPage.routeName),
|
||||
builder: (context) => LocationPickPage(
|
||||
collection: mapCollection,
|
||||
initialLocation: _mapCoordinates,
|
||||
),
|
||||
|
@ -228,8 +228,8 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||
builder: (context) => ItemPickDialog(
|
||||
settings: const RouteSettings(name: ItemPickPage.routeName),
|
||||
builder: (context) => ItemPickPage(
|
||||
collection: CollectionLens(
|
||||
source: _collection.source,
|
||||
),
|
||||
|
|
|
@ -1,309 +0,0 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/placeholder.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TagEditorPage extends StatefulWidget {
|
||||
static const routeName = '/info/tag_editor';
|
||||
|
||||
final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry;
|
||||
|
||||
const TagEditorPage({
|
||||
super.key,
|
||||
required this.filtersByEntry,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TagEditorPage> createState() => _TagEditorPageState();
|
||||
}
|
||||
|
||||
class _TagEditorPageState extends State<TagEditorPage> {
|
||||
final TextEditingController _newTagTextController = TextEditingController();
|
||||
final FocusNode _newTagTextFocusNode = FocusNode();
|
||||
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
||||
late final List<CollectionFilter> _topTags;
|
||||
late final List<PlaceholderFilter> _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place];
|
||||
final List<CollectionFilter> _userAddedFilters = [];
|
||||
|
||||
static const Color untaggedColor = Colors.blueGrey;
|
||||
|
||||
Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initTopTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final showCount = tagsByEntry.length > 1;
|
||||
final Map<CollectionFilter, int> entryCountByTag = {};
|
||||
tagsByEntry.entries.forEach((kv) {
|
||||
kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<CollectionFilter, int>> sortedTags = _sortCurrentTags(entryCountByTag);
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.tagEditorPageTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.reset),
|
||||
onPressed: _reset,
|
||||
tooltip: l10n.resetTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ValueListenableBuilder<String?>(
|
||||
valueListenable: _expandedSectionNotifier,
|
||||
builder: (context, expandedSection, child) {
|
||||
return ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _newTagTextController,
|
||||
builder: (context, value, child) {
|
||||
final upQuery = value.text.trim().toUpperCase();
|
||||
bool containQuery(CollectionFilter v) => v.getLabel(context).toUpperCase().contains(upQuery);
|
||||
final recentFilters = settings.recentTags.where(containQuery).toList();
|
||||
final topTagFilters = _topTags.where(containQuery).toList();
|
||||
final placeholderFilters = _placeholders.where(containQuery).toList();
|
||||
return ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8, end: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _newTagTextController,
|
||||
focusNode: _newTagTextFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.tagEditorPageNewTagFieldLabel,
|
||||
),
|
||||
autofocus: true,
|
||||
onSubmitted: (newTag) {
|
||||
_addCustomTag(newTag);
|
||||
_newTagTextFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _newTagTextController,
|
||||
builder: (context, value, child) {
|
||||
return IconButton(
|
||||
icon: const Icon(AIcons.add),
|
||||
onPressed: value.text.isEmpty ? null : () => _addCustomTag(_newTagTextController.text),
|
||||
tooltip: l10n.tagEditorPageAddTagTooltip,
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.tagEditorCurrentFilterSectionExpanded,
|
||||
builder: (context, isExpanded, child) {
|
||||
return IconButton(
|
||||
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
|
||||
onPressed: sortedTags.isEmpty ? null : () => settings.tagEditorCurrentFilterSectionExpanded = !isExpanded,
|
||||
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: AnimatedCrossFade(
|
||||
firstChild: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: AvesFilterChip.minChipHeight),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(AIcons.tagUntagged, color: untaggedColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.filterNoTagLabel,
|
||||
style: const TextStyle(color: untaggedColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
secondChild: ExpandableFilterRow(
|
||||
filters: sortedTags.map((kv) => kv.key).toList(),
|
||||
isExpanded: context.select<Settings, bool>((v) => v.tagEditorCurrentFilterSectionExpanded),
|
||||
removable: true,
|
||||
showGenericIcon: false,
|
||||
leadingBuilder: showCount
|
||||
? (filter) => _TagCount(
|
||||
count: sortedTags.firstWhere((kv) => kv.key == filter).value,
|
||||
)
|
||||
: null,
|
||||
onTap: _removeTag,
|
||||
onLongPress: null,
|
||||
),
|
||||
crossFadeState: sortedTags.isEmpty ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
duration: Durations.tagEditorTransition,
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
_FilterRow(
|
||||
title: l10n.statsTopTagsSectionTitle,
|
||||
filters: topTagFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
_FilterRow(
|
||||
title: l10n.tagEditorSectionRecent,
|
||||
filters: recentFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
_FilterRow(
|
||||
title: l10n.tagEditorSectionPlaceholders,
|
||||
filters: placeholderFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initTopTags() {
|
||||
final Map<String, int> entryCountByTag = {};
|
||||
final visibleEntries = context.read<CollectionSource?>()?.visibleEntries;
|
||||
visibleEntries?.forEach((entry) {
|
||||
entry.tags.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<CollectionFilter, int>> sortedTopTags = _sortCurrentTags(entryCountByTag.map((key, value) => MapEntry(TagFilter(key), value)));
|
||||
_topTags = sortedTopTags.map((kv) => kv.key).toList();
|
||||
}
|
||||
|
||||
List<MapEntry<CollectionFilter, int>> _sortCurrentTags(Map<CollectionFilter, int> entryCountByTag) {
|
||||
return entryCountByTag.entries.toList()
|
||||
..sort((kv1, kv2) {
|
||||
final filter1 = kv1.key;
|
||||
final filter2 = kv2.key;
|
||||
|
||||
final recent1 = _userAddedFilters.indexOf(filter1);
|
||||
final recent2 = _userAddedFilters.indexOf(filter2);
|
||||
var c = recent2.compareTo(recent1);
|
||||
if (c != 0) return c;
|
||||
|
||||
final count1 = kv1.value;
|
||||
final count2 = kv2.value;
|
||||
c = count2.compareTo(count1);
|
||||
if (c != 0) return c;
|
||||
|
||||
return filter1.compareTo(filter2);
|
||||
});
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_userAddedFilters.clear();
|
||||
tagsByEntry.forEach((entry, tags) {
|
||||
final Set<TagFilter> originalFilters = entry.tags.map(TagFilter.new).toSet();
|
||||
tags
|
||||
..clear()
|
||||
..addAll(originalFilters);
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _addCustomTag(String newTag) {
|
||||
if (newTag.isNotEmpty) {
|
||||
_addTag(TagFilter(newTag));
|
||||
}
|
||||
}
|
||||
|
||||
void _addTag(CollectionFilter filter) {
|
||||
settings.recentTags = settings.recentTags
|
||||
..remove(filter)
|
||||
..insert(0, filter);
|
||||
_userAddedFilters
|
||||
..remove(filter)
|
||||
..add(filter);
|
||||
tagsByEntry.forEach((entry, tags) => tags.add(filter));
|
||||
_newTagTextController.clear();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _removeTag(CollectionFilter filter) {
|
||||
_userAddedFilters.remove(filter);
|
||||
tagsByEntry.forEach((entry, filters) => filters.remove(filter));
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterRow extends StatelessWidget {
|
||||
final String title;
|
||||
final List<CollectionFilter> filters;
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
final void Function(CollectionFilter filter) onTap;
|
||||
|
||||
const _FilterRow({
|
||||
required this.title,
|
||||
required this.filters,
|
||||
required this.expandedNotifier,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return filters.isEmpty
|
||||
? const SizedBox()
|
||||
: TitledExpandableFilterRow(
|
||||
title: title,
|
||||
filters: filters,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showGenericIcon: false,
|
||||
onTap: onTap,
|
||||
onLongPress: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TagCount extends StatelessWidget {
|
||||
final int count;
|
||||
|
||||
const _TagCount({
|
||||
required this.count,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: DefaultTextStyle.of(context).style.color!,
|
||||
)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(123)),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(fontSize: AvesFilterChip.fontSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RenameEntrySetPage extends StatefulWidget {
|
||||
static const routeName = '/rename_entry_set';
|
||||
|
||||
final List<AvesEntry> entries;
|
||||
|
||||
const RenameEntrySetPage({
|
||||
super.key,
|
||||
required this.entries,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RenameEntrySetPage> createState() => _RenameEntrySetPageState();
|
||||
}
|
||||
|
||||
class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||
final TextEditingController _patternTextController = TextEditingController();
|
||||
final ValueNotifier<NamingPattern> _namingPatternNotifier = ValueNotifier<NamingPattern>(const NamingPattern([]));
|
||||
|
||||
static const int previewMax = 10;
|
||||
static const double thumbnailExtent = 48;
|
||||
|
||||
List<AvesEntry> get entries => widget.entries;
|
||||
|
||||
int get entryCount => entries.length;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_patternTextController.text = settings.entryRenamingPattern;
|
||||
_patternTextController.addListener(_onUserPatternChange);
|
||||
_onUserPatternChange();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_patternTextController.removeListener(_onUserPatternChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.renameEntrySetPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _patternTextController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.renameEntrySetPagePatternFieldLabel,
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
MenuIconTheme(
|
||||
child: PopupMenuButton<String>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: DateNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: NameNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: CounterNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.renameProcessorCounter, icon: const Icon(AIcons.counter)),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (key) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
_insertProcessor(key);
|
||||
},
|
||||
tooltip: l10n.renameEntrySetPageInsertTooltip,
|
||||
icon: const Icon(AIcons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
l10n.renameEntrySetPagePreviewSectionTitle,
|
||||
style: Constants.knownTitleTextStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.textScaleFactor,
|
||||
builder: (context, textScaleFactor, child) {
|
||||
final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor);
|
||||
return GridTheme(
|
||||
extent: effectiveThumbnailExtent,
|
||||
child: ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
final sourceName = entry.filenameWithoutExtension ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
DecoratedThumbnail(
|
||||
entry: entry,
|
||||
tileExtent: effectiveThumbnailExtent,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sourceName,
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ValueListenableBuilder<NamingPattern>(
|
||||
valueListenable: _namingPatternNotifier,
|
||||
builder: (context, pattern, child) {
|
||||
return Text(
|
||||
pattern.apply(entry, index),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: CollectionGrid.fixedExtentLayoutSpacing,
|
||||
),
|
||||
itemCount: min(entryCount, previewMax),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesOutlinedButton(
|
||||
label: l10n.entryActionRename,
|
||||
onPressed: () {
|
||||
settings.entryRenamingPattern = _patternTextController.text;
|
||||
Navigator.pop<NamingPattern>(context, _namingPatternNotifier.value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUserPatternChange() {
|
||||
_namingPatternNotifier.value = NamingPattern.from(
|
||||
userPattern: _patternTextController.text,
|
||||
entryCount: entryCount,
|
||||
);
|
||||
}
|
||||
|
||||
void _insertProcessor(String key) {
|
||||
final userPattern = _patternTextController.text;
|
||||
final selection = _patternTextController.selection;
|
||||
_patternTextController.value = _patternTextController.value.replaced(
|
||||
TextRange(
|
||||
start: NamingPattern.getInsertionOffset(userPattern, selection.start),
|
||||
end: NamingPattern.getInsertionOffset(userPattern, selection.end),
|
||||
),
|
||||
NamingPattern.defaultPatternFor(key),
|
||||
);
|
||||
}
|
||||
}
|
217
lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart
Normal file
217
lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart
Normal file
|
@ -0,0 +1,217 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RenameEntrySetPage extends StatefulWidget {
|
||||
static const routeName = '/rename_entry_set';
|
||||
|
||||
final List<AvesEntry> entries;
|
||||
|
||||
const RenameEntrySetPage({
|
||||
super.key,
|
||||
required this.entries,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RenameEntrySetPage> createState() => _RenameEntrySetPageState();
|
||||
}
|
||||
|
||||
class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||
final TextEditingController _patternTextController = TextEditingController();
|
||||
final ValueNotifier<NamingPattern> _namingPatternNotifier = ValueNotifier<NamingPattern>(const NamingPattern([]));
|
||||
|
||||
static const int previewMax = 10;
|
||||
static const double thumbnailExtent = 48;
|
||||
|
||||
List<AvesEntry> get entries => widget.entries;
|
||||
|
||||
int get entryCount => entries.length;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_patternTextController.text = settings.entryRenamingPattern;
|
||||
_patternTextController.addListener(_onUserPatternChange);
|
||||
_onUserPatternChange();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_patternTextController.removeListener(_onUserPatternChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.renameEntrySetPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _patternTextController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.renameEntrySetPagePatternFieldLabel,
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
MenuIconTheme(
|
||||
child: PopupMenuButton<String>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: DateNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: NameNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: CounterNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.renameProcessorCounter, icon: const Icon(AIcons.counter)),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (key) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||
_insertProcessor(key);
|
||||
},
|
||||
tooltip: l10n.renameEntrySetPageInsertTooltip,
|
||||
icon: const Icon(AIcons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
l10n.renameEntrySetPagePreviewSectionTitle,
|
||||
style: Constants.knownTitleTextStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.textScaleFactor,
|
||||
builder: (context, textScaleFactor, child) {
|
||||
final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor);
|
||||
return GridTheme(
|
||||
extent: effectiveThumbnailExtent,
|
||||
child: ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
final sourceName = entry.filenameWithoutExtension ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
DecoratedThumbnail(
|
||||
entry: entry,
|
||||
tileExtent: effectiveThumbnailExtent,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sourceName,
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ValueListenableBuilder<NamingPattern>(
|
||||
valueListenable: _namingPatternNotifier,
|
||||
builder: (context, pattern, child) {
|
||||
return Text(
|
||||
pattern.apply(entry, index),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: CollectionGrid.fixedExtentLayoutSpacing,
|
||||
),
|
||||
itemCount: min(entryCount, previewMax),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesOutlinedButton(
|
||||
label: l10n.entryActionRename,
|
||||
onPressed: () {
|
||||
settings.entryRenamingPattern = _patternTextController.text;
|
||||
Navigator.pop<NamingPattern>(context, _namingPatternNotifier.value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUserPatternChange() {
|
||||
_namingPatternNotifier.value = NamingPattern.from(
|
||||
userPattern: _patternTextController.text,
|
||||
entryCount: entryCount,
|
||||
);
|
||||
}
|
||||
|
||||
void _insertProcessor(String key) {
|
||||
final userPattern = _patternTextController.text;
|
||||
final selection = _patternTextController.selection;
|
||||
_patternTextController.value = _patternTextController.value.replaced(
|
||||
TextRange(
|
||||
start: NamingPattern.getInsertionOffset(userPattern, selection.start),
|
||||
end: NamingPattern.getInsertionOffset(userPattern, selection.end),
|
||||
),
|
||||
NamingPattern.defaultPatternFor(key),
|
||||
);
|
||||
}
|
||||
}
|
306
lib/widgets/dialogs/entry_editors/tag_editor_page.dart
Normal file
306
lib/widgets/dialogs/entry_editors/tag_editor_page.dart
Normal file
|
@ -0,0 +1,306 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/placeholder.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TagEditorPage extends StatefulWidget {
|
||||
static const routeName = '/info/tag_editor';
|
||||
|
||||
final Map<AvesEntry, Set<CollectionFilter>> filtersByEntry;
|
||||
|
||||
const TagEditorPage({
|
||||
super.key,
|
||||
required this.filtersByEntry,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TagEditorPage> createState() => _TagEditorPageState();
|
||||
}
|
||||
|
||||
class _TagEditorPageState extends State<TagEditorPage> {
|
||||
final TextEditingController _newTagTextController = TextEditingController();
|
||||
final FocusNode _newTagTextFocusNode = FocusNode();
|
||||
final ValueNotifier<String?> _expandedSectionNotifier = ValueNotifier(null);
|
||||
late final List<CollectionFilter> _topTags;
|
||||
late final List<PlaceholderFilter> _placeholders = [PlaceholderFilter.country, PlaceholderFilter.place];
|
||||
final List<CollectionFilter> _userAddedFilters = [];
|
||||
|
||||
static const Color untaggedColor = Colors.blueGrey;
|
||||
|
||||
Map<AvesEntry, Set<CollectionFilter>> get tagsByEntry => widget.filtersByEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initTopTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final showCount = tagsByEntry.length > 1;
|
||||
final Map<CollectionFilter, int> entryCountByTag = {};
|
||||
tagsByEntry.entries.forEach((kv) {
|
||||
kv.value.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<CollectionFilter, int>> sortedTags = _sortCurrentTags(entryCountByTag);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.tagEditorPageTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.reset),
|
||||
onPressed: _reset,
|
||||
tooltip: l10n.resetTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ValueListenableBuilder<String?>(
|
||||
valueListenable: _expandedSectionNotifier,
|
||||
builder: (context, expandedSection, child) {
|
||||
return ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _newTagTextController,
|
||||
builder: (context, value, child) {
|
||||
final upQuery = value.text.trim().toUpperCase();
|
||||
bool containQuery(CollectionFilter v) => v.getLabel(context).toUpperCase().contains(upQuery);
|
||||
final recentFilters = settings.recentTags.where(containQuery).toList();
|
||||
final topTagFilters = _topTags.where(containQuery).toList();
|
||||
final placeholderFilters = _placeholders.where(containQuery).toList();
|
||||
return ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8, end: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _newTagTextController,
|
||||
focusNode: _newTagTextFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.tagEditorPageNewTagFieldLabel,
|
||||
),
|
||||
autofocus: true,
|
||||
onSubmitted: (newTag) {
|
||||
_addCustomTag(newTag);
|
||||
_newTagTextFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _newTagTextController,
|
||||
builder: (context, value, child) {
|
||||
return IconButton(
|
||||
icon: const Icon(AIcons.add),
|
||||
onPressed: value.text.isEmpty ? null : () => _addCustomTag(_newTagTextController.text),
|
||||
tooltip: l10n.tagEditorPageAddTagTooltip,
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.tagEditorCurrentFilterSectionExpanded,
|
||||
builder: (context, isExpanded, child) {
|
||||
return IconButton(
|
||||
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
|
||||
onPressed: sortedTags.isEmpty ? null : () => settings.tagEditorCurrentFilterSectionExpanded = !isExpanded,
|
||||
tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: AnimatedCrossFade(
|
||||
firstChild: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: AvesFilterChip.minChipHeight),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(AIcons.tagUntagged, color: untaggedColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.filterNoTagLabel,
|
||||
style: const TextStyle(color: untaggedColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
secondChild: ExpandableFilterRow(
|
||||
filters: sortedTags.map((kv) => kv.key).toList(),
|
||||
isExpanded: context.select<Settings, bool>((v) => v.tagEditorCurrentFilterSectionExpanded),
|
||||
removable: true,
|
||||
showGenericIcon: false,
|
||||
leadingBuilder: showCount
|
||||
? (filter) => _TagCount(
|
||||
count: sortedTags.firstWhere((kv) => kv.key == filter).value,
|
||||
)
|
||||
: null,
|
||||
onTap: _removeTag,
|
||||
onLongPress: null,
|
||||
),
|
||||
crossFadeState: sortedTags.isEmpty ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
duration: Durations.tagEditorTransition,
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
_FilterRow(
|
||||
title: l10n.statsTopTagsSectionTitle,
|
||||
filters: topTagFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
_FilterRow(
|
||||
title: l10n.tagEditorSectionRecent,
|
||||
filters: recentFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
_FilterRow(
|
||||
title: l10n.tagEditorSectionPlaceholders,
|
||||
filters: placeholderFilters,
|
||||
expandedNotifier: _expandedSectionNotifier,
|
||||
onTap: _addTag,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initTopTags() {
|
||||
final Map<String, int> entryCountByTag = {};
|
||||
final visibleEntries = context.read<CollectionSource?>()?.visibleEntries;
|
||||
visibleEntries?.forEach((entry) {
|
||||
entry.tags.forEach((tag) => entryCountByTag[tag] = (entryCountByTag[tag] ?? 0) + 1);
|
||||
});
|
||||
List<MapEntry<CollectionFilter, int>> sortedTopTags = _sortCurrentTags(entryCountByTag.map((key, value) => MapEntry(TagFilter(key), value)));
|
||||
_topTags = sortedTopTags.map((kv) => kv.key).toList();
|
||||
}
|
||||
|
||||
List<MapEntry<CollectionFilter, int>> _sortCurrentTags(Map<CollectionFilter, int> entryCountByTag) {
|
||||
return entryCountByTag.entries.toList()
|
||||
..sort((kv1, kv2) {
|
||||
final filter1 = kv1.key;
|
||||
final filter2 = kv2.key;
|
||||
|
||||
final recent1 = _userAddedFilters.indexOf(filter1);
|
||||
final recent2 = _userAddedFilters.indexOf(filter2);
|
||||
var c = recent2.compareTo(recent1);
|
||||
if (c != 0) return c;
|
||||
|
||||
final count1 = kv1.value;
|
||||
final count2 = kv2.value;
|
||||
c = count2.compareTo(count1);
|
||||
if (c != 0) return c;
|
||||
|
||||
return filter1.compareTo(filter2);
|
||||
});
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_userAddedFilters.clear();
|
||||
tagsByEntry.forEach((entry, tags) {
|
||||
final Set<TagFilter> originalFilters = entry.tags.map(TagFilter.new).toSet();
|
||||
tags
|
||||
..clear()
|
||||
..addAll(originalFilters);
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _addCustomTag(String newTag) {
|
||||
if (newTag.isNotEmpty) {
|
||||
_addTag(TagFilter(newTag));
|
||||
}
|
||||
}
|
||||
|
||||
void _addTag(CollectionFilter filter) {
|
||||
settings.recentTags = settings.recentTags
|
||||
..remove(filter)
|
||||
..insert(0, filter);
|
||||
_userAddedFilters
|
||||
..remove(filter)
|
||||
..add(filter);
|
||||
tagsByEntry.forEach((entry, tags) => tags.add(filter));
|
||||
_newTagTextController.clear();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _removeTag(CollectionFilter filter) {
|
||||
_userAddedFilters.remove(filter);
|
||||
tagsByEntry.forEach((entry, filters) => filters.remove(filter));
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterRow extends StatelessWidget {
|
||||
final String title;
|
||||
final List<CollectionFilter> filters;
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
final void Function(CollectionFilter filter) onTap;
|
||||
|
||||
const _FilterRow({
|
||||
required this.title,
|
||||
required this.filters,
|
||||
required this.expandedNotifier,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return filters.isEmpty
|
||||
? const SizedBox()
|
||||
: TitledExpandableFilterRow(
|
||||
title: title,
|
||||
filters: filters,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showGenericIcon: false,
|
||||
onTap: onTap,
|
||||
onLongPress: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TagCount extends StatelessWidget {
|
||||
final int count;
|
||||
|
||||
const _TagCount({
|
||||
required this.count,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: DefaultTextStyle.of(context).style.color!,
|
||||
)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(123)),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(fontSize: AvesFilterChip.fontSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,10 +13,10 @@ import 'package:aves/utils/constants.dart';
|
|||
import 'package:aves/widgets/common/basic/color_list_tile.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/dialogs/app_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/app_pick_page.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -346,8 +346,8 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||
builder: (context) => ItemPickDialog(
|
||||
settings: const RouteSettings(name: ItemPickPage.routeName),
|
||||
builder: (context) => ItemPickPage(
|
||||
collection: CollectionLens(
|
||||
source: context.read<CollectionSource>(),
|
||||
filters: {filter},
|
||||
|
@ -367,8 +367,8 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
final package = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: AppPickDialog.routeName),
|
||||
builder: (context) => AppPickDialog(
|
||||
settings: const RouteSettings(name: AppPickPage.routeName),
|
||||
builder: (context) => AppPickPage(
|
||||
initialValue: _customPackage,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
|
|
144
lib/widgets/dialogs/pick_dialogs/app_pick_page.dart
Normal file
144
lib/widgets/dialogs/pick_dialogs/app_pick_page.dart
Normal file
|
@ -0,0 +1,144 @@
|
|||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppPickPage extends StatefulWidget {
|
||||
static const routeName = '/app_pick';
|
||||
|
||||
final String? initialValue;
|
||||
|
||||
const AppPickPage({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AppPickPage> createState() => _AppPickPageState();
|
||||
}
|
||||
|
||||
class _AppPickPageState extends State<AppPickPage> {
|
||||
late String? _selectedValue;
|
||||
late Future<Set<Package>> _loader;
|
||||
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||
|
||||
static const double iconSize = 32;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedValue = widget.initialValue;
|
||||
_loader = androidAppService.getPackages();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.appPickDialogTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<Set<Package>>(
|
||||
future: _loader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final allPackages = snapshot.data;
|
||||
if (allPackages == null) return const SizedBox();
|
||||
final packages = allPackages.where((package) => package.categoryLauncher).toList()..sort((a, b) => compareAsciiUpperCase(_displayName(a), _displayName(b)));
|
||||
return Column(
|
||||
children: [
|
||||
QueryBar(queryNotifier: _queryNotifier),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _queryNotifier,
|
||||
builder: (context, query, child) {
|
||||
final upQuery = query.toUpperCase().trim();
|
||||
final visiblePackages = packages.where((package) {
|
||||
return {
|
||||
package.packageName,
|
||||
package.currentLabel,
|
||||
package.englishLabel,
|
||||
...package.potentialDirs,
|
||||
}.any((v) => v != null && v.toUpperCase().contains(upQuery));
|
||||
}).toList();
|
||||
final showNoneOption = upQuery.isEmpty;
|
||||
final itemCount = visiblePackages.length + (showNoneOption ? 1 : 0);
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (showNoneOption) {
|
||||
if (index == 0) {
|
||||
return ReselectableRadioListTile<String?>(
|
||||
value: '',
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text(
|
||||
context.l10n.appPickDialogNone,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
index--;
|
||||
}
|
||||
|
||||
final package = visiblePackages[index];
|
||||
return ReselectableRadioListTile<String?>(
|
||||
value: package.packageName,
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: Image(
|
||||
image: AppIconImage(
|
||||
packageName: package.packageName,
|
||||
size: iconSize,
|
||||
),
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: _displayName(package),
|
||||
),
|
||||
],
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: itemCount,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _displayName(Package package) => package.currentLabel ?? package.packageName;
|
||||
}
|
|
@ -5,28 +5,27 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ItemPickDialog extends StatefulWidget {
|
||||
class ItemPickPage extends StatefulWidget {
|
||||
static const routeName = '/item_pick';
|
||||
|
||||
final CollectionLens collection;
|
||||
|
||||
const ItemPickDialog({
|
||||
const ItemPickPage({
|
||||
super.key,
|
||||
required this.collection,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ItemPickDialog> createState() => _ItemPickDialogState();
|
||||
State<ItemPickPage> createState() => _ItemPickPageState();
|
||||
}
|
||||
|
||||
class _ItemPickDialogState extends State<ItemPickDialog> {
|
||||
class _ItemPickPageState extends State<ItemPickPage> {
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
@override
|
||||
|
@ -40,7 +39,6 @@ class _ItemPickDialogState extends State<ItemPickDialog> {
|
|||
final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
|
||||
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||
value: ValueNotifier(AppMode.pickMediaInternal),
|
||||
child: MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: SelectionProvider<AvesEntry>(
|
||||
child: QueryProvider(
|
||||
|
@ -60,7 +58,6 @@ class _ItemPickDialogState extends State<ItemPickDialog> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,20 +14,19 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/map/geo_map.dart';
|
||||
import 'package:aves/widgets/common/providers/map_theme_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves_map/aves_map.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LocationPickDialog extends StatelessWidget {
|
||||
class LocationPickPage extends StatelessWidget {
|
||||
static const routeName = '/location_pick';
|
||||
|
||||
final CollectionLens? collection;
|
||||
final LatLng? initialLocation;
|
||||
|
||||
const LocationPickDialog({
|
||||
const LocationPickPage({
|
||||
super.key,
|
||||
required this.collection,
|
||||
required this.initialLocation,
|
||||
|
@ -35,8 +34,7 @@ class LocationPickDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
left: false,
|
||||
top: false,
|
||||
|
@ -47,7 +45,6 @@ class LocationPickDialog extends StatelessWidget {
|
|||
initialLocation: initialLocation,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/query.dart';
|
||||
|
@ -24,7 +25,6 @@ import 'package:aves/widgets/common/grid/sliver.dart';
|
|||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
|
@ -37,6 +37,7 @@ import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
|||
import 'package:aves/widgets/filter_grids/common/section_layout.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -76,19 +77,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Selector<Settings, bool>(
|
||||
selector: (context, s) => s.enableBottomNavigationBar,
|
||||
builder: (context, enableBottomNavigationBar, child) {
|
||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
||||
return NotificationListener<DraggableScrollBarNotification>(
|
||||
onNotification: (notification) {
|
||||
_draggableScrollBarEventStreamController.add(notification.event);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: QueryProvider(
|
||||
final body = QueryProvider(
|
||||
initialQuery: null,
|
||||
child: WillPopScope(
|
||||
onWillPop: () {
|
||||
|
@ -133,7 +122,33 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (device.isTelevision) {
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
const TvRail(),
|
||||
Expanded(child: body),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
);
|
||||
} else {
|
||||
return Selector<Settings, bool>(
|
||||
selector: (context, s) => s.enableBottomNavigationBar,
|
||||
builder: (context, enableBottomNavigationBar, child) {
|
||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
||||
|
||||
return NotificationListener<DraggableScrollBarNotification>(
|
||||
onNotification: (notification) {
|
||||
_draggableScrollBarEventStreamController.add(notification.event);
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: body,
|
||||
drawer: canNavigate ? const AppDrawer() : null,
|
||||
bottomNavigationBar: showBottomNavigationBar
|
||||
? AppBottomNavBar(
|
||||
|
@ -145,10 +160,10 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
||||
final String? settingsRouteKey;
|
||||
|
|
|
@ -23,7 +23,6 @@ import 'package:aves/widgets/common/identity/empty.dart';
|
|||
import 'package:aves/widgets/common/map/geo_map.dart';
|
||||
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/map_theme_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:aves/widgets/map/map_info_row.dart';
|
||||
|
@ -56,7 +55,6 @@ class MapPage extends StatelessWidget {
|
|||
// as the map can be stacked on top of other pages
|
||||
// that catch highlight events and will not let it bubble up
|
||||
return HighlightInfoProvider(
|
||||
child: MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
left: false,
|
||||
|
@ -70,7 +68,6 @@ class MapPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -230,25 +230,21 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
return [
|
||||
const Divider(),
|
||||
...pageBookmarks.map((route) {
|
||||
WidgetBuilder? pageBuilder;
|
||||
Widget? trailing;
|
||||
switch (route) {
|
||||
case AlbumListPage.routeName:
|
||||
pageBuilder = (_) => const AlbumListPage();
|
||||
trailing = StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, _) => Text('${source.rawAlbums.length}'),
|
||||
);
|
||||
break;
|
||||
case CountryListPage.routeName:
|
||||
pageBuilder = (_) => const CountryListPage();
|
||||
trailing = StreamBuilder(
|
||||
stream: source.eventBus.on<CountriesChangedEvent>(),
|
||||
builder: (context, _) => Text('${source.sortedCountries.length}'),
|
||||
);
|
||||
break;
|
||||
case TagListPage.routeName:
|
||||
pageBuilder = (_) => const TagListPage();
|
||||
trailing = StreamBuilder(
|
||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||
builder: (context, _) => Text('${source.sortedTags.length}'),
|
||||
|
@ -261,7 +257,6 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
key: Key('drawer-page-$route'),
|
||||
trailing: trailing,
|
||||
routeName: route,
|
||||
pageBuilder: pageBuilder ?? (_) => const SizedBox(),
|
||||
);
|
||||
}),
|
||||
];
|
||||
|
@ -281,11 +276,10 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget get debugTile => PageNavTile(
|
||||
Widget get debugTile => const PageNavTile(
|
||||
// key is expected by test driver
|
||||
key: const Key('drawer-debug'),
|
||||
key: Key('drawer-debug'),
|
||||
topLevel: false,
|
||||
routeName: AppDebugPage.routeName,
|
||||
pageBuilder: (_) => const AppDebugPage(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ class AlbumNavTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
var filter = AlbumFilter(album, source.getAlbumDisplayName(context, album));
|
||||
final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album));
|
||||
return CollectionNavTile(
|
||||
leading: DrawerFilterIcon(filter: filter),
|
||||
title: DrawerFilterTitle(filter: filter),
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
import 'package:aves/widgets/about/about_page.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/tile.dart';
|
||||
import 'package:aves/widgets/settings/settings_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PageNavTile extends StatelessWidget {
|
||||
final Widget? trailing;
|
||||
final bool topLevel;
|
||||
final String routeName;
|
||||
final WidgetBuilder? pageBuilder;
|
||||
|
||||
const PageNavTile({
|
||||
super.key,
|
||||
this.trailing,
|
||||
this.topLevel = true,
|
||||
required this.routeName,
|
||||
required this.pageBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _pageBuilder = pageBuilder;
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
|
@ -37,12 +40,11 @@ class PageNavTile extends StatelessWidget {
|
|||
),
|
||||
)
|
||||
: null,
|
||||
onTap: _pageBuilder != null
|
||||
? () {
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
final route = MaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: _pageBuilder,
|
||||
builder: pageBuilder(routeName),
|
||||
);
|
||||
if (topLevel) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
|
@ -53,10 +55,28 @@ class PageNavTile extends StatelessWidget {
|
|||
} else {
|
||||
Navigator.push(context, route);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
},
|
||||
selected: context.currentRouteName == routeName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static WidgetBuilder pageBuilder(String route) {
|
||||
switch (route) {
|
||||
case AlbumListPage.routeName:
|
||||
return (_) => const AlbumListPage();
|
||||
case CountryListPage.routeName:
|
||||
return (_) => const CountryListPage();
|
||||
case TagListPage.routeName:
|
||||
return (_) => const TagListPage();
|
||||
case SettingsPage.routeName:
|
||||
return (_) => const SettingsPage();
|
||||
case AboutPage.routeName:
|
||||
return (_) => const AboutPage();
|
||||
case AppDebugPage.routeName:
|
||||
return (_) => const AppDebugPage();
|
||||
default:
|
||||
throw Exception('unknown route=$route');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,6 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/navigation/nav_display.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -52,16 +49,14 @@ class DrawerPageIcon extends StatelessWidget {
|
|||
final icon = NavigationDisplay.getPageIcon(route);
|
||||
if (icon != null) {
|
||||
switch (route) {
|
||||
case AlbumListPage.routeName:
|
||||
case CountryListPage.routeName:
|
||||
case TagListPage.routeName:
|
||||
return Icon(icon);
|
||||
case AppDebugPage.routeName:
|
||||
return ShaderMask(
|
||||
shaderCallback: AvesColorsData.debugGradient.createShader,
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Icon(icon),
|
||||
);
|
||||
default:
|
||||
return Icon(icon);
|
||||
}
|
||||
}
|
||||
return const SizedBox();
|
||||
|
|
|
@ -3,11 +3,13 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/about/about_page.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/settings/settings_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -35,6 +37,10 @@ class NavigationDisplay {
|
|||
return l10n.drawerCountryPage;
|
||||
case TagListPage.routeName:
|
||||
return l10n.drawerTagPage;
|
||||
case SettingsPage.routeName:
|
||||
return l10n.settingsPageTitle;
|
||||
case AboutPage.routeName:
|
||||
return l10n.aboutPageTitle;
|
||||
case AppDebugPage.routeName:
|
||||
return 'Debug';
|
||||
default:
|
||||
|
@ -50,6 +56,10 @@ class NavigationDisplay {
|
|||
return AIcons.location;
|
||||
case TagListPage.routeName:
|
||||
return AIcons.tag;
|
||||
case SettingsPage.routeName:
|
||||
return AIcons.settings;
|
||||
case AboutPage.routeName:
|
||||
return AIcons.info;
|
||||
case AppDebugPage.routeName:
|
||||
return AIcons.debug;
|
||||
default:
|
||||
|
|
194
lib/widgets/navigation/tv_rail.dart
Normal file
194
lib/widgets/navigation/tv_rail.dart
Normal file
|
@ -0,0 +1,194 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/about/about_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/tile.dart';
|
||||
import 'package:aves/widgets/settings/settings_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TvRail extends StatefulWidget {
|
||||
// collection loaded in the `CollectionPage`, if any
|
||||
final CollectionLens? currentCollection;
|
||||
|
||||
const TvRail({
|
||||
super.key,
|
||||
this.currentCollection,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TvRail> createState() => _TvRailState();
|
||||
}
|
||||
|
||||
class _TvRailState extends State<TvRail> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
CollectionLens? get currentCollection => widget.currentCollection;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final header = Row(
|
||||
children: [
|
||||
const AvesLogo(size: 48),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
context.l10n.appName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 44,
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 1.0,
|
||||
fontFeatures: [FontFeature.enable('smcp')],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final navEntries = <_NavEntry>[
|
||||
..._buildTypeLinks(),
|
||||
..._buildAlbumLinks(context),
|
||||
..._buildPageLinks(context),
|
||||
...[
|
||||
SettingsPage.routeName,
|
||||
AboutPage.routeName,
|
||||
].map(_routeNavEntry),
|
||||
if (!kReleaseMode) _routeNavEntry(AppDebugPage.routeName),
|
||||
];
|
||||
|
||||
final rail = NavigationRail(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
extended: true,
|
||||
destinations: navEntries
|
||||
.map((v) => NavigationRailDestination(
|
||||
icon: v.icon,
|
||||
label: v.label,
|
||||
))
|
||||
.toList(),
|
||||
selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))),
|
||||
onDestinationSelected: (index) => navEntries[index].onSelection(),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
header,
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(child: rail),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<_NavEntry> _buildTypeLinks() {
|
||||
final hiddenFilters = settings.hiddenFilters;
|
||||
final typeBookmarks = settings.drawerTypeBookmarks;
|
||||
final currentFilters = currentCollection?.filters;
|
||||
return typeBookmarks.where((filter) => !hiddenFilters.contains(filter)).map((filter) {
|
||||
bool isSelected() {
|
||||
if (currentFilters == null || currentFilters.length > 1) return false;
|
||||
return currentFilters.firstOrNull == filter;
|
||||
}
|
||||
|
||||
return _NavEntry(
|
||||
icon: DrawerFilterIcon(filter: filter),
|
||||
label: DrawerFilterTitle(filter: filter),
|
||||
isSelected: isSelected(),
|
||||
onSelection: () => _goToCollection(context, filter),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<_NavEntry> _buildAlbumLinks(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final currentFilters = currentCollection?.filters;
|
||||
final albums = settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context);
|
||||
return albums.map((album) {
|
||||
final filter = AlbumFilter(album, source.getAlbumDisplayName(context, album));
|
||||
bool isSelected() {
|
||||
if (currentFilters == null || currentFilters.length > 1) return false;
|
||||
final currentFilter = currentFilters.firstOrNull;
|
||||
return currentFilter is AlbumFilter && currentFilter.album == album;
|
||||
}
|
||||
|
||||
return _NavEntry(
|
||||
icon: DrawerFilterIcon(filter: filter),
|
||||
label: DrawerFilterTitle(filter: filter),
|
||||
isSelected: isSelected(),
|
||||
onSelection: () => _goToCollection(context, filter),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<_NavEntry> _buildPageLinks(BuildContext context) {
|
||||
final pageBookmarks = settings.drawerPageBookmarks;
|
||||
return pageBookmarks.map(_routeNavEntry).toList();
|
||||
}
|
||||
|
||||
_NavEntry _routeNavEntry(String route) => _NavEntry(
|
||||
icon: DrawerPageIcon(route: route),
|
||||
label: DrawerPageTitle(route: route),
|
||||
isSelected: context.currentRouteName == route,
|
||||
onSelection: () => _goTo(route),
|
||||
);
|
||||
|
||||
Future<void> _goTo(String routeName) async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: PageNavTile.pageBuilder(routeName),
|
||||
));
|
||||
}
|
||||
|
||||
void _goToCollection(BuildContext context, CollectionFilter? filter) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: context.read<CollectionSource>(),
|
||||
filters: {filter},
|
||||
),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class _NavEntry {
|
||||
final Widget icon;
|
||||
final Widget label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onSelection;
|
||||
|
||||
const _NavEntry({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onSelection,
|
||||
});
|
||||
}
|
|
@ -5,7 +5,6 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/utils/constants.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/settings/common/quick_actions/action_button.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/action_panel.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/available_actions.dart';
|
||||
|
@ -38,8 +37,7 @@ class QuickActionEditorPage<T extends Object> extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
|
@ -53,7 +51,6 @@ class QuickActionEditorPage<T extends Object> extends StatelessWidget {
|
|||
save: save,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import 'package:aves/utils/constants.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/home_widget.dart';
|
||||
import 'package:aves/widgets/settings/common/collection_tile.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
|
@ -68,8 +67,7 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settingsWidgetPageTitle),
|
||||
),
|
||||
|
@ -122,7 +120,6 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'package:aves/utils/constants.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:aves/widgets/settings/language/locale.dart';
|
||||
import 'package:aves/widgets/settings/language/locale_tile.dart';
|
||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.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/settings/language/locales.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LocaleTile extends StatelessWidget {
|
||||
static const systemLocaleOption = Locale('system');
|
||||
|
||||
const LocaleTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
// key is expected by test driver
|
||||
key: const Key('tile-language'),
|
||||
title: Text(context.l10n.settingsLanguageTile),
|
||||
subtitle: Selector<Settings, Locale?>(
|
||||
selector: (context, s) => settings.locale,
|
||||
builder: (context, locale, child) {
|
||||
return Text(locale == null ? context.l10n.settingsSystemDefault : getLocaleName(locale));
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
final value = await Navigator.push<Locale>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: LocaleSelectionPage.routeName),
|
||||
builder: (context) => const LocaleSelectionPage(),
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.pageTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.locale = value == systemLocaleOption ? null : value;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static String getLocaleName(Locale locale) {
|
||||
// the package `flutter_localized_locales` has the answer for all locales
|
||||
// but it comes with 3 MB of assets
|
||||
return SupportedLocales.languagesByLanguageCode[locale.languageCode] ?? locale.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class LocaleSelectionPage extends StatefulWidget {
|
||||
static const routeName = '/settings/locale';
|
||||
|
||||
const LocaleSelectionPage({super.key});
|
||||
|
||||
@override
|
||||
State<LocaleSelectionPage> createState() => _LocaleSelectionPageState();
|
||||
}
|
||||
|
||||
class _LocaleSelectionPageState extends State<LocaleSelectionPage> {
|
||||
late Locale _selectedValue;
|
||||
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedValue = settings.locale ?? LocaleTile.systemLocaleOption;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsLanguagePageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: _queryNotifier,
|
||||
builder: (context, query, child) {
|
||||
final upQuery = query.toUpperCase().trim();
|
||||
return ListView(
|
||||
children: [
|
||||
QueryBar(
|
||||
queryNotifier: _queryNotifier,
|
||||
leadingPadding: const EdgeInsetsDirectional.only(start: 24, end: 8),
|
||||
),
|
||||
..._getLocaleOptions(context).entries.where((kv) {
|
||||
if (upQuery.isEmpty) return true;
|
||||
final title = kv.value;
|
||||
return title.toUpperCase().contains(upQuery);
|
||||
}).map((kv) {
|
||||
final value = kv.key;
|
||||
final title = kv.value;
|
||||
return ReselectableRadioListTile<Locale>(
|
||||
// key is expected by test driver
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LinkedHashMap<Locale, String> _getLocaleOptions(BuildContext context) {
|
||||
final displayLocales = AvesApp.supportedLocales.map((locale) => MapEntry(locale, LocaleTile.getLocaleName(locale))).toList()..sort((a, b) => compareAsciiUpperCase(a.value, b.value));
|
||||
|
||||
return LinkedHashMap.of({
|
||||
LocaleTile.systemLocaleOption: context.l10n.settingsSystemDefault,
|
||||
...LinkedHashMap.fromEntries(displayLocales),
|
||||
});
|
||||
}
|
||||
}
|
86
lib/widgets/settings/language/locale_selection_page.dart
Normal file
86
lib/widgets/settings/language/locale_selection_page.dart
Normal file
|
@ -0,0 +1,86 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/language/locale_tile.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LocaleSelectionPage extends StatefulWidget {
|
||||
static const routeName = '/settings/locale';
|
||||
|
||||
const LocaleSelectionPage({super.key});
|
||||
|
||||
@override
|
||||
State<LocaleSelectionPage> createState() => _LocaleSelectionPageState();
|
||||
}
|
||||
|
||||
class _LocaleSelectionPageState extends State<LocaleSelectionPage> {
|
||||
late Locale _selectedValue;
|
||||
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedValue = settings.locale ?? LocaleTile.systemLocaleOption;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsLanguagePageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: _queryNotifier,
|
||||
builder: (context, query, child) {
|
||||
final upQuery = query.toUpperCase().trim();
|
||||
return ListView(
|
||||
children: [
|
||||
QueryBar(
|
||||
queryNotifier: _queryNotifier,
|
||||
leadingPadding: const EdgeInsetsDirectional.only(start: 24, end: 8),
|
||||
),
|
||||
..._getLocaleOptions(context).entries.where((kv) {
|
||||
if (upQuery.isEmpty) return true;
|
||||
final title = kv.value;
|
||||
return title.toUpperCase().contains(upQuery);
|
||||
}).map((kv) {
|
||||
final value = kv.key;
|
||||
final title = kv.value;
|
||||
return ReselectableRadioListTile<Locale>(
|
||||
// key is expected by test driver
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => Navigator.pop(context, v),
|
||||
reselectable: true,
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LinkedHashMap<Locale, String> _getLocaleOptions(BuildContext context) {
|
||||
final displayLocales = AvesApp.supportedLocales.map((locale) => MapEntry(locale, LocaleTile.getLocaleName(locale))).toList()..sort((a, b) => compareAsciiUpperCase(a.value, b.value));
|
||||
|
||||
return LinkedHashMap.of({
|
||||
LocaleTile.systemLocaleOption: context.l10n.settingsSystemDefault,
|
||||
...LinkedHashMap.fromEntries(displayLocales),
|
||||
});
|
||||
}
|
||||
}
|
49
lib/widgets/settings/language/locale_tile.dart
Normal file
49
lib/widgets/settings/language/locale_tile.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/language/locale_selection_page.dart';
|
||||
import 'package:aves/widgets/settings/language/locales.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LocaleTile extends StatelessWidget {
|
||||
static const systemLocaleOption = Locale('system');
|
||||
|
||||
const LocaleTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
// key is expected by test driver
|
||||
key: const Key('tile-language'),
|
||||
title: Text(context.l10n.settingsLanguageTile),
|
||||
subtitle: Selector<Settings, Locale?>(
|
||||
selector: (context, s) => settings.locale,
|
||||
builder: (context, locale, child) {
|
||||
return Text(locale == null ? context.l10n.settingsSystemDefault : getLocaleName(locale));
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
final value = await Navigator.push<Locale>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: LocaleSelectionPage.routeName),
|
||||
builder: (context) => const LocaleSelectionPage(),
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.pageTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.locale = value == systemLocaleOption ? null : value;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static String getLocaleName(Locale locale) {
|
||||
// the package `flutter_localized_locales` has the answer for all locales
|
||||
// but it comes with 3 MB of assets
|
||||
return SupportedLocales.languagesByLanguageCode[locale.languageCode] ?? locale.toString();
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StorageAccessPage extends StatefulWidget {
|
||||
static const routeName = '/settings/storage_access';
|
||||
|
||||
const StorageAccessPage({super.key});
|
||||
|
||||
@override
|
||||
State<StorageAccessPage> createState() => _StorageAccessPageState();
|
||||
}
|
||||
|
||||
class _StorageAccessPageState extends State<StorageAccessPage> {
|
||||
late Future<List<String>> _pathLoader;
|
||||
List<String>? _lastPaths;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
void _load() => _pathLoader = storageService.getGrantedDirectories();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsStorageAccessPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<List<String>>(
|
||||
future: _pathLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done && _lastPaths == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
_lastPaths = snapshot.data!..sort();
|
||||
if (_lastPaths!.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _Header(),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: EmptyContent(
|
||||
text: context.l10n.settingsStorageAccessEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
const _Header(),
|
||||
const Divider(),
|
||||
..._lastPaths!.map((path) => ListTile(
|
||||
title: Text(path),
|
||||
dense: true,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(AIcons.clear),
|
||||
onPressed: () async {
|
||||
await storageService.revokeDirectoryAccess(path);
|
||||
_load();
|
||||
setState(() {});
|
||||
},
|
||||
tooltip: context.l10n.settingsStorageAccessRevokeTooltip,
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(AIcons.info),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: Text(context.l10n.settingsStorageAccessBanner)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
105
lib/widgets/settings/privacy/access_grants_page.dart
Normal file
105
lib/widgets/settings/privacy/access_grants_page.dart
Normal file
|
@ -0,0 +1,105 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StorageAccessPage extends StatefulWidget {
|
||||
static const routeName = '/settings/storage_access';
|
||||
|
||||
const StorageAccessPage({super.key});
|
||||
|
||||
@override
|
||||
State<StorageAccessPage> createState() => _StorageAccessPageState();
|
||||
}
|
||||
|
||||
class _StorageAccessPageState extends State<StorageAccessPage> {
|
||||
late Future<List<String>> _pathLoader;
|
||||
List<String>? _lastPaths;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
void _load() => _pathLoader = storageService.getGrantedDirectories();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsStorageAccessPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: FutureBuilder<List<String>>(
|
||||
future: _pathLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done && _lastPaths == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
_lastPaths = snapshot.data!..sort();
|
||||
if (_lastPaths!.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _Header(),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: EmptyContent(
|
||||
text: context.l10n.settingsStorageAccessEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
const _Header(),
|
||||
const Divider(),
|
||||
..._lastPaths!.map((path) => ListTile(
|
||||
title: Text(path),
|
||||
dense: true,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(AIcons.clear),
|
||||
onPressed: () async {
|
||||
await storageService.revokeDirectoryAccess(path);
|
||||
_load();
|
||||
setState(() {});
|
||||
},
|
||||
tooltip: context.l10n.settingsStorageAccessRevokeTooltip,
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(AIcons.info),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: Text(context.l10n.settingsStorageAccessBanner)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,23 +10,22 @@ import 'package:aves/widgets/common/basic/menu.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class FilePicker extends StatefulWidget {
|
||||
class FilePickerPage extends StatefulWidget {
|
||||
static const routeName = '/file_picker';
|
||||
|
||||
const FilePicker({super.key});
|
||||
const FilePickerPage({super.key});
|
||||
|
||||
@override
|
||||
State<FilePicker> createState() => _FilePickerState();
|
||||
State<FilePickerPage> createState() => _FilePickerPageState();
|
||||
}
|
||||
|
||||
class _FilePickerState extends State<FilePicker> {
|
||||
class _FilePickerPageState extends State<FilePickerPage> {
|
||||
late VolumeRelativeDirectory _directory;
|
||||
List<Directory>? _contents;
|
||||
|
||||
|
@ -65,7 +64,6 @@ class _FilePickerState extends State<FilePicker> {
|
|||
setState(() {});
|
||||
return SynchronousFuture(false);
|
||||
},
|
||||
child: MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_getTitle(context)),
|
||||
|
@ -138,7 +136,6 @@ class _FilePickerState extends State<FilePicker> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -7,8 +7,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/privacy/file_picker/file_picker.dart';
|
||||
import 'package:aves/widgets/settings/privacy/file_picker/file_picker_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -33,8 +32,7 @@ class HiddenItemsPage extends StatelessWidget {
|
|||
),
|
||||
];
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: DefaultTabController(
|
||||
return DefaultTabController(
|
||||
length: tabs.length,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
@ -49,7 +47,6 @@ class HiddenItemsPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -148,8 +145,8 @@ class _HiddenPaths extends StatelessWidget {
|
|||
final path = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: const RouteSettings(name: FilePicker.routeName),
|
||||
builder: (context) => const FilePicker(),
|
||||
settings: const RouteSettings(name: FilePickerPage.routeName),
|
||||
builder: (context) => const FilePickerPage(),
|
||||
),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
|
@ -9,8 +9,8 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:aves/widgets/settings/privacy/access_grants.dart';
|
||||
import 'package:aves/widgets/settings/privacy/hidden_items.dart';
|
||||
import 'package:aves/widgets/settings/privacy/access_grants_page.dart';
|
||||
import 'package:aves/widgets/settings/privacy/hidden_items_page.dart';
|
||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:aves/model/settings/enums/slideshow_video_playback.dart';
|
|||
import 'package:aves/model/settings/enums/viewer_transition.dart';
|
||||
import 'package:aves/model/settings/settings.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/settings/common/collection_tile.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -18,8 +17,7 @@ class ScreenSaverSettingsPage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settingsScreenSaverPageTitle),
|
||||
),
|
||||
|
@ -68,7 +66,6 @@ class ScreenSaverSettingsPage extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,8 +63,7 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final durations = context.watch<DurationsData>();
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: InteractiveAppBarTitle(
|
||||
onTap: () => _goToSearch(context),
|
||||
|
@ -133,7 +132,6 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||
import 'package:aves/model/settings/settings.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/settings/common/quick_actions/editor_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
@ -42,8 +41,7 @@ class CollectionActionEditorPage extends StatelessWidget {
|
|||
),
|
||||
];
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: DefaultTabController(
|
||||
return DefaultTabController(
|
||||
length: tabs.length,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
@ -58,7 +56,6 @@ class CollectionActionEditorPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||
import 'package:aves/widgets/settings/thumbnails/collection_actions_editor.dart';
|
||||
import 'package:aves/widgets/settings/thumbnails/collection_actions_editor_page.dart';
|
||||
import 'package:aves/widgets/settings/thumbnails/overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||
import 'package:aves/widgets/settings/video/video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -19,8 +18,7 @@ class _VideoSettingsPageState extends State<VideoSettingsPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsVideoPageTitle),
|
||||
),
|
||||
|
@ -45,7 +43,6 @@ class _VideoSettingsPageState extends State<VideoSettingsPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:aves/widgets/stats/date/histogram.dart';
|
||||
import 'package:aves/widgets/stats/filter_table.dart';
|
||||
|
@ -224,8 +223,7 @@ class _StatsPageState extends State<StatsPage> {
|
|||
}
|
||||
}
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.statsPageTitle),
|
||||
),
|
||||
|
@ -240,7 +238,6 @@ class _StatsPageState extends State<StatsPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -355,8 +352,7 @@ class StatsTopPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
|
@ -380,7 +376,6 @@ class StatsTopPage extends StatelessWidget {
|
|||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/viewer/controller.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
|
@ -42,8 +41,7 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: ViewStateConductorProvider(
|
||||
child: VideoConductorProvider(
|
||||
child: MultiPageConductorProvider(
|
||||
|
@ -61,7 +59,6 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
|
|||
? Colors.black
|
||||
: Colors.white,
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||
import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart';
|
||||
|
@ -46,8 +45,7 @@ class _InfoPageState extends State<InfoPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
|
@ -90,7 +88,6 @@ class _InfoPageState extends State<InfoPage> {
|
|||
),
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import 'package:aves/widgets/aves_app.dart';
|
|||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -60,7 +59,6 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
_onLeave();
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
|
@ -143,7 +141,6 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/viewer/controller.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
|
||||
|
@ -94,10 +93,8 @@ class _ScreenSaverPageState extends State<ScreenSaverPage> with WidgetsBindingOb
|
|||
}
|
||||
}
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/viewer/controller.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
|
||||
|
@ -60,7 +59,6 @@ class _SlideshowPageState extends State<SlideshowPage> {
|
|||
final entries = _slideshowCollection.sortedEntries;
|
||||
return ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||
value: ValueNotifier(AppMode.slideshow),
|
||||
child: MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: entries.isEmpty
|
||||
? EmptyContent(
|
||||
|
@ -86,7 +84,6 @@ class _SlideshowPageState extends State<SlideshowPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'package:aves/services/common/services.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/viewer/controller.dart';
|
||||
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
|
@ -35,8 +34,7 @@ class WallpaperPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: entry != null
|
||||
? ViewStateConductorProvider(
|
||||
child: VideoConductorProvider(
|
||||
|
@ -50,7 +48,6 @@ class WallpaperPage extends StatelessWidget {
|
|||
: const SizedBox(),
|
||||
backgroundColor: Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white,
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:aves/widgets/common/basic/markdown_container.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/home_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -47,8 +46,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: FutureBuilder<String>(
|
||||
|
@ -110,7 +108,6 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue