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