#437 tv: rail (wip)

This commit is contained in:
Thibault Deckers 2022-12-09 12:51:36 +01:00
parent 053ff71949
commit 3a151638e8
51 changed files with 1964 additions and 1796 deletions

View file

@ -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 {
),
),
),
),
);
}
}

View file

@ -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!),
),
),
);

View file

@ -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> {
);
},
);
}
},
),
),
);
}

View file

@ -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,
),

View file

@ -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';

View file

@ -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,
),
),
);
}
}

View file

@ -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> {
),
),
),
),
);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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,
),

View file

@ -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,
),

View file

@ -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),
),
);
}
}

View file

@ -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),
);
}
}

View 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),
);
}
}

View 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),
),
);
}
}

View file

@ -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,

View 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;
}

View file

@ -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> {
),
),
),
),
);
}
}

View file

@ -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,
),
),
),
);
}
}

View file

@ -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,9 +160,9 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
),
);
},
),
);
}
}
}
class FilterGrid<T extends CollectionFilter> extends StatefulWidget {

View file

@ -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 {
),
),
),
),
);
}
}

View file

@ -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(),
);
}

View file

@ -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),

View file

@ -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');
}
}
}

View file

@ -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();

View file

@ -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:

View 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,
});
}

View file

@ -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,
),
),
),
);
}
}

View file

@ -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> {
],
),
),
),
);
}

View file

@ -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';

View file

@ -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),
});
}
}

View 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),
});
}
}

View 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();
}
}

View file

@ -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)),
],
),
);
}
}

View 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)),
],
),
);
}
}

View file

@ -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> {
),
),
),
),
);
}

View file

@ -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

View file

@ -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';

View file

@ -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 {
],
),
),
),
);
}
}

View file

@ -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 {
),
),
),
),
);
}

View file

@ -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 {
),
),
),
),
);
}
}

View file

@ -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';

View file

@ -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> {
),
),
),
),
);
}
}

View file

@ -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 {
}),
),
),
),
);
}
}

View file

@ -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,
),
);
}
}

View file

@ -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,
),
);
}

View file

@ -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,
),
),
);
}

View file

@ -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,
),
);
}

View file

@ -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> {
),
),
),
),
);
}

View file

@ -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,
),
);
}
}

View file

@ -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> {
),
),
),
),
);
}