explorer: custom home, shortcut
This commit is contained in:
parent
3d424eb82b
commit
fbd498bee8
21 changed files with 343 additions and 58 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
23
lib/view/src/actions/explorer.dart
Normal file
23
lib/view/src/actions/explorer.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
75
lib/widgets/dialogs/select_storage_dialog.dart
Normal file
75
lib/widgets/dialogs/select_storage_dialog.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -20,4 +20,4 @@ Future<void> showSelectionDialog<T>({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef TextBuilder<T> = String Function(T value);
|
typedef TextBuilder<T> = String? Function(T value);
|
||||||
|
|
|
@ -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,11 +112,50 @@ 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 _volumes.map((v) {
|
return [
|
||||||
|
ExplorerAction.addShortcut,
|
||||||
|
ExplorerAction.setHome,
|
||||||
|
].map((v) {
|
||||||
|
return PopupMenuItem(
|
||||||
|
value: v,
|
||||||
|
child: MenuRow(text: v.getText(context), icon: v.getIcon()),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
onSelected: (action) async {
|
||||||
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
|
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
||||||
|
final directory = widget.directoryNotifier.value;
|
||||||
|
ExplorerActionDelegate(directory: directory).onActionSelected(context, action);
|
||||||
|
},
|
||||||
|
popUpAnimationStyle: animations.popUpAnimationStyle,
|
||||||
|
),
|
||||||
|
].map((v) => FontSizeIconTheme(child: v)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 selected = widget.directoryNotifier.value.volumePath == v.path;
|
||||||
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
|
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
|
||||||
return PopupMenuItem(
|
return PopupMenuItem(
|
||||||
|
@ -124,16 +167,20 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
},
|
final volumePath = widget.directoryNotifier.value.volumePath;
|
||||||
onSelected: (volume) async {
|
final initialVolume = _volumes.firstWhereOrNull((v) => v.path == volumePath);
|
||||||
// wait for the popup menu to hide before proceeding with the action
|
final volume = await showDialog<StorageVolume?>(
|
||||||
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
context: context,
|
||||||
|
builder: (context) => SelectStorageDialog(initialVolume: initialVolume),
|
||||||
|
routeSettings: const RouteSettings(name: SelectStorageDialog.routeName),
|
||||||
|
);
|
||||||
|
if (volume != null) {
|
||||||
widget.goTo(volume.path);
|
widget.goTo(volume.path);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
popUpAnimationStyle: animations.popUpAnimationStyle,
|
tooltip: context.l10n.explorerActionSelectStorageVolume,
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double get appBarContentHeight {
|
double get appBarContentHeight {
|
||||||
|
|
85
lib/widgets/explorer/explorer_action_delegate.dart
Normal file
85
lib/widgets/explorer/explorer_action_delegate.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
4
plugins/aves_model/lib/src/actions/explorer.dart
Normal file
4
plugins/aves_model/lib/src/actions/explorer.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
enum ExplorerAction {
|
||||||
|
addShortcut,
|
||||||
|
setHome,
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue