explorer: custom home, shortcut

This commit is contained in:
Thibault Deckers 2024-07-15 23:53:46 +02:00
parent 3d424eb82b
commit fbd498bee8
21 changed files with 343 additions and 58 deletions

View file

@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Explorer: set custom path as home
- Explorer: create shortcut to custom path
- predictive back support (inter-app) - predictive back support (inter-app)
### Changed ### Changed

View file

@ -294,11 +294,9 @@ open class MainActivity : FlutterActivity() {
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) { if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
fields[INTENT_DATA_KEY_SAFE_MODE] = true fields[INTENT_DATA_KEY_SAFE_MODE] = true
} }
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page -> fields[INTENT_DATA_KEY_PAGE] = intent.getStringExtra(EXTRA_KEY_PAGE)
val filters = extractFiltersFromIntent(intent) fields[INTENT_DATA_KEY_FILTERS] = extractFiltersFromIntent(intent)
fields[INTENT_DATA_KEY_PAGE] = page fields[INTENT_DATA_KEY_EXPLORER_PATH] = intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH)
fields[INTENT_DATA_KEY_FILTERS] = filters
}
return fields return fields
} }
@ -527,6 +525,7 @@ open class MainActivity : FlutterActivity() {
const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness" const val INTENT_DATA_KEY_BRIGHTNESS = "brightness"
const val INTENT_DATA_KEY_EXPLORER_PATH = "explorerPath"
const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page" const val INTENT_DATA_KEY_PAGE = "page"
@ -537,6 +536,7 @@ open class MainActivity : FlutterActivity() {
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page" const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_EXPLORER_PATH = "explorerPath"
const val EXTRA_KEY_FILTERS_ARRAY = "filters" const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString" const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_SAFE_MODE = "safeMode" const val EXTRA_KEY_SAFE_MODE = "safeMode"

View file

@ -19,6 +19,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
@ -351,8 +352,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val label = call.argument<String>("label") val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes") val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters") val filters = call.argument<List<String>>("filters")
val explorerPath = call.argument<String>("explorerPath")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (label == null || (filters == null && uri == null)) { if (label == null) {
result.error("pin-args", "missing arguments", null) result.error("pin-args", "missing arguments", null)
return return
} }
@ -380,7 +382,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
val intent = when { val intent = when {
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/collection") .putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
@ -388,6 +389,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// so we use a joined `String` as fallback // so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/explorer")
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
else -> { else -> {
result.error("pin-intent", "failed to build intent", null) result.error("pin-intent", "failed to build intent", null)
return return

View file

@ -771,6 +771,9 @@
"binPageTitle": "Recycle Bin", "binPageTitle": "Recycle Bin",
"explorerPageTitle": "Explorer", "explorerPageTitle": "Explorer",
"explorerActionSelectStorageVolume": "Select storage",
"selectStorageVolumeDialogTitle": "Select Storage",
"searchCollectionFieldHint": "Search collection", "searchCollectionFieldHint": "Search collection",
"searchRecentSectionTitle": "Recent", "searchRecentSectionTitle": "Recent",
@ -804,7 +807,7 @@
"settingsNavigationSectionTitle": "Navigation", "settingsNavigationSectionTitle": "Navigation",
"settingsHomeTile": "Home", "settingsHomeTile": "Home",
"settingsHomeDialogTitle": "Home", "settingsHomeDialogTitle": "Home",
"setHomeCustomCollection": "Custom collection", "setHomeCustom": "Custom",
"settingsShowBottomNavigationBar": "Show bottom navigation bar", "settingsShowBottomNavigationBar": "Show bottom navigation bar",
"settingsKeepScreenOnTile": "Keep screen on", "settingsKeepScreenOnTile": "Keep screen on",
"settingsKeepScreenOnDialogTitle": "Keep Screen On", "settingsKeepScreenOnDialogTitle": "Keep Screen On",

View file

@ -14,11 +14,19 @@ mixin NavigationSettings on SettingsAccess {
HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values); HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
set homePage(HomePageSetting newValue) => set(SettingKeys.homePageKey, newValue.toString());
Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set homeCustomCollection(Set<CollectionFilter> newValue) => set(SettingKeys.homeCustomCollectionKey, newValue.map((filter) => filter.toJson()).toList()); String? get homeCustomExplorerPath => getString(SettingKeys.homeCustomExplorerPathKey);
void setHome(
HomePageSetting homePage, {
Set<CollectionFilter> customCollection = const {},
String? customExplorerPath,
}) {
set(SettingKeys.homePageKey, homePage.toString());
set(SettingKeys.homeCustomCollectionKey, customCollection.map((filter) => filter.toJson()).toList());
set(SettingKeys.homeCustomExplorerPathKey, customExplorerPath);
}
bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar; bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar;

View file

@ -440,6 +440,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
case SettingKeys.maxBrightnessKey: case SettingKeys.maxBrightnessKey:
case SettingKeys.keepScreenOnKey: case SettingKeys.keepScreenOnKey:
case SettingKeys.homePageKey: case SettingKeys.homePageKey:
case SettingKeys.homeCustomExplorerPathKey:
case SettingKeys.collectionGroupFactorKey: case SettingKeys.collectionGroupFactorKey:
case SettingKeys.collectionSortFactorKey: case SettingKeys.collectionSortFactorKey:
case SettingKeys.thumbnailLocationIconKey: case SettingKeys.thumbnailLocationIconKey:

View file

@ -30,7 +30,7 @@ abstract class AppService {
Future<bool> shareSingle(String uri, String mimeType); Future<bool> shareSingle(String uri, String mimeType);
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}); Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri});
} }
class PlatformAppService implements AppService { class PlatformAppService implements AppService {
@ -203,7 +203,7 @@ class PlatformAppService implements AppService {
// app shortcuts // app shortcuts
@override @override
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async { Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri}) async {
Uint8List? iconBytes; Uint8List? iconBytes;
if (coverEntry != null) { if (coverEntry != null) {
final size = coverEntry.isVideo ? 0.0 : 256.0; final size = coverEntry.isVideo ? 0.0 : 256.0;
@ -222,6 +222,7 @@ class PlatformAppService implements AppService {
'label': label, 'label': label,
'iconBytes': iconBytes, 'iconBytes': iconBytes,
'filters': filters?.map((filter) => filter.toJson()).toList(), 'filters': filters?.map((filter) => filter.toJson()).toList(),
'explorerPath': explorerPath,
'uri': uri, 'uri': uri,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {

View file

@ -0,0 +1,23 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
extension ExtraExplorerActionView on ExplorerAction {
String getText(BuildContext context) {
final l10n = context.l10n;
return switch (this) {
ExplorerAction.addShortcut => l10n.collectionActionAddShortcut,
ExplorerAction.setHome => l10n.collectionActionSetHome,
};
}
Widget getIcon() => Icon(_getIconData());
IconData _getIconData() {
return switch (this) {
ExplorerAction.addShortcut => AIcons.addShortcut,
ExplorerAction.setHome => AIcons.home,
};
}
}

View file

@ -753,8 +753,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
} }
void _setHome(BuildContext context) async { void _setHome(BuildContext context) async {
settings.homeCustomCollection = context.read<CollectionLens>().filters; settings.setHome(HomePageSetting.collection, customCollection: context.read<CollectionLens>().filters);
settings.homePage = HomePageSetting.collection;
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
} }
} }

View file

@ -0,0 +1,75 @@
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class SelectStorageDialog extends StatefulWidget {
static const routeName = '/dialog/select_storage';
final StorageVolume? initialVolume;
const SelectStorageDialog({super.key, this.initialVolume});
@override
State<SelectStorageDialog> createState() => _SelectStorageDialogState();
}
class _SelectStorageDialogState extends State<SelectStorageDialog> {
late Set<StorageVolume> _allVolumes;
late StorageVolume? _primaryVolume, _selectedVolume;
@override
void initState() {
super.initState();
_allVolumes = androidFileUtils.storageVolumes;
_primaryVolume = _allVolumes.firstWhereOrNull((volume) => volume.isPrimary) ?? _allVolumes.firstOrNull;
_selectedVolume = widget.initialVolume ?? _primaryVolume;
}
@override
Widget build(BuildContext context) {
final byPrimary = groupBy<StorageVolume, bool>(_allVolumes, (volume) => volume.isPrimary);
int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCaseNatural(a.path, b.path);
final primaryVolumes = (byPrimary[true] ?? [])..sort(compare);
final otherVolumes = (byPrimary[false] ?? [])..sort(compare);
return AvesDialog(
title: context.l10n.selectStorageVolumeDialogTitle,
scrollableContent: [
...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)),
...otherVolumes.map((volume) => _buildVolumeTile(context, volume)),
],
actions: [
const CancelButton(),
TextButton(
onPressed: () => Navigator.maybeOf(context)?.pop(_selectedVolume),
child: Text(context.l10n.applyButtonLabel),
),
],
);
}
Widget _buildVolumeTile(BuildContext context, StorageVolume volume) => RadioListTile<StorageVolume>(
value: volume,
groupValue: _selectedVolume,
onChanged: (volume) {
_selectedVolume = volume!;
setState(() {});
},
title: Text(
volume.getDescription(context),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
subtitle: Text(
volume.path,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}

View file

@ -20,4 +20,4 @@ Future<void> showSelectionDialog<T>({
} }
} }
typedef TextBuilder<T> = String Function(T value); typedef TextBuilder<T> = String? Function(T value);

View file

@ -7,6 +7,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/src/actions/explorer.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
@ -15,9 +16,12 @@ import 'package:aves/widgets/common/basic/popup/menu_row.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_app_bar.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/dialogs/select_storage_dialog.dart';
import 'package:aves/widgets/explorer/explorer_action_delegate.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.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:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.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';
@ -108,32 +112,75 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
onPressed: () => _goToSearch(context), onPressed: () => _goToSearch(context),
tooltip: MaterialLocalizations.of(context).searchFieldLabel, tooltip: MaterialLocalizations.of(context).searchFieldLabel,
), ),
if (_volumes.length > 1) if (_volumes.length > 1) _buildVolumeSelector(context),
FontSizeIconTheme( PopupMenuButton<ExplorerAction>(
child: PopupMenuButton<StorageVolume>( itemBuilder: (context) {
itemBuilder: (context) { return [
return _volumes.map((v) { ExplorerAction.addShortcut,
final selected = widget.directoryNotifier.value.volumePath == v.path; ExplorerAction.setHome,
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain; ].map((v) {
return PopupMenuItem( return PopupMenuItem(
value: v, value: v,
enabled: !selected, child: MenuRow(text: v.getText(context), icon: v.getIcon()),
child: MenuRow( );
text: v.getDescription(context), }).toList();
icon: Icon(icon), },
), onSelected: (action) async {
); // wait for the popup menu to hide before proceeding with the action
}).toList(); await Future.delayed(animations.popUpAnimationDelay * timeDilation);
}, final directory = widget.directoryNotifier.value;
onSelected: (volume) async { ExplorerActionDelegate(directory: directory).onActionSelected(context, action);
// wait for the popup menu to hide before proceeding with the action },
await Future.delayed(animations.popUpAnimationDelay * timeDilation); popUpAnimationStyle: animations.popUpAnimationStyle,
widget.goTo(volume.path); ),
}, ].map((v) => FontSizeIconTheme(child: v)).toList();
popUpAnimationStyle: animations.popUpAnimationStyle, }
),
), Widget _buildVolumeSelector(BuildContext context) {
]; if (_volumes.length == 2) {
return ValueListenableBuilder<VolumeRelativeDirectory>(
valueListenable: widget.directoryNotifier,
builder: (context, directory, child) {
final currentVolume = directory.volumePath;
final otherVolume = _volumes.firstWhere((volume) => volume.path != currentVolume);
final icon = otherVolume.isRemovable ? AIcons.storageCard : AIcons.storageMain;
return IconButton(
icon: Icon(icon),
onPressed: () => widget.goTo(otherVolume.path),
tooltip: otherVolume.getDescription(context),
);
},
);
} else {
return IconButton(
icon: const Icon(AIcons.storageCard),
onPressed: () async {
_volumes.map((v) {
final selected = widget.directoryNotifier.value.volumePath == v.path;
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
return PopupMenuItem(
value: v,
enabled: !selected,
child: MenuRow(
text: v.getDescription(context),
icon: Icon(icon),
),
);
}).toList();
final volumePath = widget.directoryNotifier.value.volumePath;
final initialVolume = _volumes.firstWhereOrNull((v) => v.path == volumePath);
final volume = await showDialog<StorageVolume?>(
context: context,
builder: (context) => SelectStorageDialog(initialVolume: initialVolume),
routeSettings: const RouteSettings(name: SelectStorageDialog.routeName),
);
if (volume != null) {
widget.goTo(volume.path);
}
},
tooltip: context.l10n.explorerActionSelectStorageVolume,
);
}
} }
double get appBarContentHeight { double get appBarContentHeight {

View file

@ -0,0 +1,85 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/path.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/services/common/services.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ExplorerActionDelegate with FeedbackMixin {
final VolumeRelativeDirectory directory;
ExplorerActionDelegate({required this.directory});
bool isVisible(
ExplorerAction action, {
required AppMode appMode,
}) {
final isMain = appMode == AppMode.main;
final useTvLayout = settings.useTvLayout;
switch (action) {
case ExplorerAction.addShortcut:
return isMain && device.canPinShortcut;
case ExplorerAction.setHome:
return isMain && !useTvLayout;
}
}
bool canApply(ExplorerAction action) {
switch (action) {
case ExplorerAction.addShortcut:
case ExplorerAction.setHome:
return true;
}
}
void onActionSelected(BuildContext context, ExplorerAction action) {
reportService.log('$action');
switch (action) {
case ExplorerAction.addShortcut:
_addShortcut(context);
case ExplorerAction.setHome:
_setHome(context);
}
}
Future<void> _addShortcut(BuildContext context) async {
final path = directory.dirPath;
final filter = PathFilter(path);
final defaultName = filter.getLabel(context);
final collection = CollectionLens(
source: context.read<CollectionSource>(),
filters: {filter},
);
final result = await showDialog<(AvesEntry?, String)>(
context: context,
builder: (context) => AddShortcutDialog(
defaultName: defaultName,
collection: collection,
),
routeSettings: const RouteSettings(name: AddShortcutDialog.routeName),
);
if (result == null) return;
final (coverEntry, name) = result;
if (name.isEmpty) return;
await appService.pinToHomeScreen(name, coverEntry, explorerPath: path);
if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}
void _setHome(BuildContext context) async {
settings.setHome(HomePageSetting.explorer, customExplorerPath: directory.dirPath);
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}

View file

@ -61,11 +61,13 @@ class _HomePageState extends State<HomePage> {
int? _widgetId; int? _widgetId;
String? _initialRouteName, _initialSearchQuery; String? _initialRouteName, _initialSearchQuery;
Set<CollectionFilter>? _initialFilters; Set<CollectionFilter>? _initialFilters;
String? _initialExplorerPath;
List<String>? _secureUris; List<String>? _secureUris;
static const allowedShortcutRoutes = [ static const allowedShortcutRoutes = [
CollectionPage.routeName,
AlbumListPage.routeName, AlbumListPage.routeName,
CollectionPage.routeName,
ExplorerPage.routeName,
SearchPage.routeName, SearchPage.routeName,
]; ];
@ -92,6 +94,7 @@ class _HomePageState extends State<HomePage> {
final safeMode = intentData[IntentDataKeys.safeMode] ?? false; final safeMode = intentData[IntentDataKeys.safeMode] ?? false;
final intentAction = intentData[IntentDataKeys.action]; final intentAction = intentData[IntentDataKeys.action];
_initialFilters = null; _initialFilters = null;
_initialExplorerPath = null;
_secureUris = null; _secureUris = null;
await androidFileUtils.init(); await androidFileUtils.init();
@ -186,6 +189,7 @@ class _HomePageState extends State<HomePage> {
final extraFilters = intentData[IntentDataKeys.filters]; final extraFilters = intentData[IntentDataKeys.filters];
_initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null; _initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
} }
_initialExplorerPath = intentData[IntentDataKeys.explorerPath];
} }
context.read<ValueNotifier<AppMode>>().value = appMode; context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString())); unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
@ -351,7 +355,8 @@ class _HomePageState extends State<HomePage> {
case TagListPage.routeName: case TagListPage.routeName:
return buildRoute((context) => const TagListPage()); return buildRoute((context) => const TagListPage());
case ExplorerPage.routeName: case ExplorerPage.routeName:
return buildRoute((context) => const ExplorerPage()); final path = _initialExplorerPath ?? settings.homeCustomExplorerPath;
return buildRoute((context) => ExplorerPage(path: path));
case HomeWidgetSettingsPage.routeName: case HomeWidgetSettingsPage.routeName:
return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!)); return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!));
case ScreenSaverPage.routeName: case ScreenSaverPage.routeName:

View file

@ -15,6 +15,7 @@ class IntentDataKeys {
static const action = 'action'; static const action = 'action';
static const allowMultiple = 'allowMultiple'; static const allowMultiple = 'allowMultiple';
static const brightness = 'brightness'; static const brightness = 'brightness';
static const explorerPath = 'explorerPath';
static const filters = 'filters'; static const filters = 'filters';
static const mimeType = 'mimeType'; static const mimeType = 'mimeType';
static const page = 'page'; static const page = 'page';

View file

@ -2,8 +2,10 @@ import 'dart:async';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.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/theme/text.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.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';
@ -43,24 +45,44 @@ class NavigationSection extends SettingsSection {
class _HomeOption { class _HomeOption {
final HomePageSetting page; final HomePageSetting page;
final Set<CollectionFilter> customCollection; final Set<CollectionFilter> customCollection;
final String? customExplorerPath;
const _HomeOption( const _HomeOption(
this.page, { this.page, {
this.customCollection = const {}, this.customCollection = const {},
this.customExplorerPath,
}); });
String getName(BuildContext context) { String getName(BuildContext context) {
if (page == HomePageSetting.collection && customCollection.isNotEmpty) { final pageName = page.getName(context);
return context.l10n.setHomeCustomCollection; switch (page) {
case HomePageSetting.collection:
return customCollection.isNotEmpty ? context.l10n.setHomeCustom : pageName;
case HomePageSetting.explorer:
return customExplorerPath != null ? context.l10n.setHomeCustom : pageName;
default:
return pageName;
}
}
String? getDetails(BuildContext context) {
switch (page) {
case HomePageSetting.collection:
final filters = customCollection;
return filters.isNotEmpty ? [context.l10n.collectionPageTitle, filters.map((v) => v.getLabel(context)).join(', ')].join(AText.separator) : null;
case HomePageSetting.explorer:
final path = customExplorerPath;
return path != null ? [context.l10n.explorerPageTitle, pContext.basename(path)].join(AText.separator) : null;
default:
return null;
} }
return page.getName(context);
} }
@override @override
bool operator ==(Object other) => identical(this, other) || other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection); bool operator ==(Object other) => identical(this, other) || (other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection) && customExplorerPath == other.customExplorerPath);
@override @override
int get hashCode => page.hashCode ^ customCollection.hashCode; int get hashCode => page.hashCode ^ customCollection.hashCode ^ customExplorerPath.hashCode;
} }
class SettingsTileNavigationHomePage extends SettingsTile { class SettingsTileNavigationHomePage extends SettingsTile {
@ -75,15 +97,18 @@ class SettingsTileNavigationHomePage extends SettingsTile {
const _HomeOption(HomePageSetting.tags), const _HomeOption(HomePageSetting.tags),
const _HomeOption(HomePageSetting.explorer), const _HomeOption(HomePageSetting.explorer),
if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection), if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection),
if (settings.homeCustomExplorerPath != null) _HomeOption(HomePageSetting.explorer, customExplorerPath: settings.homeCustomExplorerPath),
], ],
getName: (context, v) => v.getName(context), getName: (context, v) => v.getName(context),
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection), selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection, customExplorerPath: s.homeCustomExplorerPath),
onSelection: (v) { onSelection: (v) => settings.setHome(
settings.homePage = v.page; v.page,
settings.homeCustomCollection = v.customCollection; customCollection: v.customCollection,
}, customExplorerPath: v.customExplorerPath,
),
tileTitle: title(context), tileTitle: title(context),
dialogTitle: context.l10n.settingsHomeDialogTitle, dialogTitle: context.l10n.settingsHomeDialogTitle,
optionSubtitleBuilder: (v) => v.getDetails(context),
); );
} }

View file

@ -1,6 +1,7 @@
library aves_model; library aves_model;
export 'src/actions/chip.dart'; export 'src/actions/chip.dart';
export 'src/actions/explorer.dart';
export 'src/actions/chip_set.dart'; export 'src/actions/chip_set.dart';
export 'src/actions/entry.dart'; export 'src/actions/entry.dart';
export 'src/actions/entry_set.dart'; export 'src/actions/entry_set.dart';

View file

@ -0,0 +1,4 @@
enum ExplorerAction {
addShortcut,
setHome,
}

View file

@ -43,6 +43,7 @@ class SettingKeys {
static const keepScreenOnKey = 'keep_screen_on'; static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page'; static const homePageKey = 'home_page';
static const homeCustomCollectionKey = 'home_custom_collection'; static const homeCustomCollectionKey = 'home_custom_collection';
static const homeCustomExplorerPathKey = 'home_custom_explorer_path';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmCreateVaultKey = 'confirm_create_vault'; static const confirmCreateVaultKey = 'confirm_create_vault';
static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmDeleteForeverKey = 'confirm_delete_forever';

View file

@ -30,8 +30,7 @@ Future<void> configureAndLaunch() async {
..enableBlurEffect = true ..enableBlurEffect = true
// navigation // navigation
..keepScreenOn = KeepScreenOn.always ..keepScreenOn = KeepScreenOn.always
..homePage = HomePageSetting.collection ..setHome(HomePageSetting.collection)
..homeCustomCollection = {}
..enableBottomNavigationBar = true ..enableBottomNavigationBar = true
..drawerTypeBookmarks = [null, FavouriteFilter.instance] ..drawerTypeBookmarks = [null, FavouriteFilter.instance]
// collection // collection

View file

@ -26,8 +26,7 @@ Future<void> configureAndLaunch() async {
..enableBlurEffect = true ..enableBlurEffect = true
// navigation // navigation
..keepScreenOn = KeepScreenOn.always ..keepScreenOn = KeepScreenOn.always
..homePage = HomePageSetting.collection ..setHome(HomePageSetting.collection)
..homeCustomCollection = {}
..enableBottomNavigationBar = true ..enableBottomNavigationBar = true
// collection // collection
..collectionSectionFactor = EntryGroupFactor.album ..collectionSectionFactor = EntryGroupFactor.album