#52 hidden paths
This commit is contained in:
parent
97e3063998
commit
1f7e70697e
12 changed files with 242 additions and 6 deletions
|
@ -114,7 +114,7 @@ class MainActivity : FlutterActivity() {
|
|||
MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK)
|
||||
}
|
||||
}
|
||||
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST -> {
|
||||
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST, SELECT_DIRECTORY_REQUEST -> {
|
||||
onPermissionResult(requestCode, data?.data)
|
||||
}
|
||||
}
|
||||
|
@ -196,6 +196,7 @@ class MainActivity : FlutterActivity() {
|
|||
const val DELETE_PERMISSION_REQUEST = 2
|
||||
const val CREATE_FILE_REQUEST = 3
|
||||
const val OPEN_FILE_REQUEST = 4
|
||||
const val SELECT_DIRECTORY_REQUEST = 5
|
||||
|
||||
// permission request code to pending runnable
|
||||
val pendingResultHandlers = ConcurrentHashMap<Int, PendingResultHandler>()
|
||||
|
|
|
@ -10,6 +10,7 @@ import deckers.thibault.aves.MainActivity
|
|||
import deckers.thibault.aves.PendingResultHandler
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.PermissionManager
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.EventChannel.EventSink
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -41,6 +42,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
"requestVolumeAccess" -> requestVolumeAccess()
|
||||
"createFile" -> GlobalScope.launch(Dispatchers.IO) { createFile() }
|
||||
"openFile" -> GlobalScope.launch(Dispatchers.IO) { openFile() }
|
||||
"selectDirectory" -> GlobalScope.launch(Dispatchers.IO) { selectDirectory() }
|
||||
else -> endOfStream()
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +130,24 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
|
||||
}
|
||||
|
||||
private fun selectDirectory() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
|
||||
MainActivity.pendingResultHandlers[MainActivity.SELECT_DIRECTORY_REQUEST] = PendingResultHandler(null, { uri ->
|
||||
success(StorageUtils.convertTreeUriToDirPath(activity, uri))
|
||||
endOfStream()
|
||||
}, {
|
||||
success(null)
|
||||
endOfStream()
|
||||
})
|
||||
activity.startActivityForResult(intent, MainActivity.SELECT_DIRECTORY_REQUEST)
|
||||
} else {
|
||||
success(null)
|
||||
endOfStream()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
|
||||
private fun success(result: Any?) {
|
||||
|
|
|
@ -659,6 +659,19 @@
|
|||
"settingsHiddenFiltersEmpty": "No hidden filters",
|
||||
"@settingsHiddenFiltersEmpty": {},
|
||||
|
||||
"settingsHiddenPathsTile": "Hidden paths",
|
||||
"@settingsHiddenPathsTile": {},
|
||||
"settingsHiddenPathsTitle": "Hidden Paths",
|
||||
"@settingsHiddenPathsTitle": {},
|
||||
"settingsHiddenPathsBanner": "Photos and videos in these folders, or any of their subfolders, will not appear in your collection.",
|
||||
"@settingsHiddenPathsBanner": {},
|
||||
"settingsHiddenPathsEmpty": "No hidden paths",
|
||||
"@settingsHiddenPathsEmpty": {},
|
||||
"settingsHiddenPathsRemoveTooltip": "Remove",
|
||||
"@settingsHiddenPathsRemoveTooltip": {},
|
||||
"addPathTooltip": "Add path",
|
||||
"@addPathTooltip": {},
|
||||
|
||||
"settingsStorageAccessTile": "Storage access",
|
||||
"@settingsStorageAccessTile": {},
|
||||
"settingsStorageAccessTitle": "Storage Access",
|
||||
|
|
|
@ -313,6 +313,13 @@
|
|||
"settingsHiddenFiltersBanner": "이 필터에 맞는 사진과 동영상이 숨겨지고 있으며 이 앱에서 보여지지 않을 것입니다.",
|
||||
"settingsHiddenFiltersEmpty": "숨겨진 필터가 없습니다",
|
||||
|
||||
"settingsHiddenPathsTile": "숨겨진 경로",
|
||||
"settingsHiddenPathsTitle": "숨겨진 경로",
|
||||
"settingsHiddenPathsBanner": "이 경로에 있는 사진과 동영상이 숨겨지고 있으며 이 앱에서 보여지지 않을 것입니다.",
|
||||
"settingsHiddenPathsEmpty": "숨겨진 경로가 없습니다",
|
||||
"settingsHiddenPathsRemoveTooltip": "제거",
|
||||
"addPathTooltip": "경로 추가",
|
||||
|
||||
"settingsStorageAccessTile": "저장공간 접근",
|
||||
"settingsStorageAccessTitle": "저장공간 접근",
|
||||
"settingsStorageAccessBanner": "어떤 폴더는 사용자의 허용을 받아야만 앱이 파일에 접근이 가능합니다. 이 화면에 허용을 받은 폴더를 확인할 수 있으며 원하지 않으면 취소할 수 있습니다.",
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/filters/album.dart';
|
|||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/type.dart';
|
||||
|
@ -22,6 +23,7 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
|||
AlbumFilter.type,
|
||||
LocationFilter.type,
|
||||
TagFilter.type,
|
||||
PathFilter.type,
|
||||
];
|
||||
|
||||
static CollectionFilter? fromJson(String jsonString) {
|
||||
|
@ -43,6 +45,8 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
|||
return QueryFilter.fromMap(jsonMap);
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
}
|
||||
}
|
||||
debugPrint('failed to parse filter from json=$jsonString');
|
||||
|
@ -65,7 +69,7 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
|||
|
||||
String getTooltip(BuildContext context) => getLabel(context);
|
||||
|
||||
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false});
|
||||
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => null;
|
||||
|
||||
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context)));
|
||||
|
||||
|
|
46
lib/model/filters/path.dart
Normal file
46
lib/model/filters/path.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class PathFilter extends CollectionFilter {
|
||||
static const type = 'path';
|
||||
|
||||
final String path;
|
||||
|
||||
const PathFilter(this.path);
|
||||
|
||||
PathFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['path'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'path': path,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => entry.directory?.startsWith(path) ?? false;
|
||||
|
||||
@override
|
||||
String get universalLabel => path;
|
||||
|
||||
@override
|
||||
String get category => type;
|
||||
|
||||
@override
|
||||
String get key => '$type-$path';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is PathFilter && other.path == path;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(type, path);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{path=$path}';
|
||||
}
|
|
@ -33,6 +33,8 @@ abstract class StorageService {
|
|||
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
|
||||
|
||||
Future<Uint8List> openFile(String mimeType);
|
||||
|
||||
Future<String?> selectDirectory();
|
||||
}
|
||||
|
||||
class PlatformStorageService implements StorageService {
|
||||
|
@ -217,4 +219,25 @@ class PlatformStorageService implements StorageService {
|
|||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> selectDirectory() async {
|
||||
try {
|
||||
final completer = Completer<String>();
|
||||
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'selectDirectory',
|
||||
}).listen(
|
||||
(data) => completer.complete(data as String?),
|
||||
onError: completer.completeError,
|
||||
onDone: () {
|
||||
if (!completer.isCompleted) completer.complete(null);
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
return completer.future;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('selectDirectory failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ class AIcons {
|
|||
static const IconData tagOff = MdiIcons.tagOffOutline;
|
||||
|
||||
// actions
|
||||
static const IconData addPath = Icons.add_circle_outline;
|
||||
static const IconData addShortcut = Icons.add_to_home_screen_outlined;
|
||||
static const IconData replay10 = Icons.replay_10_outlined;
|
||||
static const IconData skip10 = Icons.forward_10_outlined;
|
||||
|
|
|
@ -77,7 +77,7 @@ class SectionHeader<T> extends StatelessWidget {
|
|||
}
|
||||
|
||||
void _toggleSectionSelection(BuildContext context) {
|
||||
final sectionEntries = context.read<SectionedListLayout<T>>().sections[sectionKey]!;
|
||||
final sectionEntries = context.read<SectionedListLayout<T>>().sections[sectionKey] ?? [];
|
||||
final selection = context.read<Selection<T>>();
|
||||
final isSelected = selection.isSelected(sectionEntries);
|
||||
if (isSelected) {
|
||||
|
@ -189,7 +189,7 @@ class _SectionSelectingLeading<T> extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sectionEntries = context.watch<SectionedListLayout<T>>().sections[sectionKey]!;
|
||||
final sectionEntries = context.watch<SectionedListLayout<T>>().sections[sectionKey] ?? [];
|
||||
final selection = context.watch<Selection<T>>();
|
||||
final isSelected = selection.isSelected(sectionEntries);
|
||||
return AnimatedSwitcher(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -29,7 +30,7 @@ class HiddenFilterTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
class HiddenFilterPage extends StatelessWidget {
|
||||
static const routeName = '/settings/hidden';
|
||||
static const routeName = '/settings/hidden_filters';
|
||||
|
||||
const HiddenFilterPage({Key? key}) : super(key: key);
|
||||
|
||||
|
@ -41,7 +42,7 @@ class HiddenFilterPage extends StatelessWidget {
|
|||
),
|
||||
body: SafeArea(
|
||||
child: Selector<Settings, Set<CollectionFilter>>(
|
||||
selector: (context, s) => settings.hiddenFilters,
|
||||
selector: (context, s) => settings.hiddenFilters.where((v) => v is! PathFilter).toSet(),
|
||||
builder: (context, hiddenFilters, child) {
|
||||
if (hiddenFilters.isEmpty) {
|
||||
return Column(
|
||||
|
|
118
lib/widgets/settings/privacy/hidden_paths.dart
Normal file
118
lib/widgets/settings/privacy/hidden_paths.dart
Normal file
|
@ -0,0 +1,118 @@
|
|||
import 'package:aves/model/filters/path.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/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';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HiddenPathTile extends StatelessWidget {
|
||||
const HiddenPathTile({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(context.l10n.settingsHiddenPathsTile),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: HiddenPathPage.routeName),
|
||||
builder: (context) => const HiddenPathPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenPathPage extends StatelessWidget {
|
||||
static const routeName = '/settings/hidden_paths';
|
||||
|
||||
const HiddenPathPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsHiddenPathsTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(AIcons.addPath),
|
||||
onPressed: () async {
|
||||
final path = await storageService.selectDirectory();
|
||||
if (path != null && path.isNotEmpty) {
|
||||
context.read<CollectionSource>().changeFilterVisibility({PathFilter(path)}, false);
|
||||
}
|
||||
},
|
||||
tooltip: context.l10n.addPathTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Selector<Settings, Set<PathFilter>>(
|
||||
selector: (context, s) => settings.hiddenFilters.whereType<PathFilter>().toSet(),
|
||||
builder: (context, hiddenPaths, child) {
|
||||
if (hiddenPaths.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _Header(),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: EmptyContent(
|
||||
icon: AIcons.hide,
|
||||
text: context.l10n.settingsHiddenPathsEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final pathList = hiddenPaths.toList()..sort();
|
||||
return ListView(
|
||||
children: [
|
||||
const _Header(),
|
||||
const Divider(),
|
||||
...pathList.map((pathFilter) => ListTile(
|
||||
title: Text(pathFilter.path),
|
||||
dense: true,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(AIcons.clear),
|
||||
onPressed: () {
|
||||
context.read<CollectionSource>().changeFilterVisibility({pathFilter}, true);
|
||||
},
|
||||
tooltip: context.l10n.settingsHiddenPathsRemoveTooltip,
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header({Key? key}) : super(key: key);
|
||||
|
||||
@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.settingsHiddenPathsBanner)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
|||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/privacy/access_grants.dart';
|
||||
import 'package:aves/widgets/settings/privacy/hidden_filters.dart';
|
||||
import 'package:aves/widgets/settings/privacy/hidden_paths.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -47,6 +48,7 @@ class PrivacySection extends StatelessWidget {
|
|||
title: Text(context.l10n.settingsSaveSearchHistory),
|
||||
),
|
||||
const HiddenFilterTile(),
|
||||
const HiddenPathTile(),
|
||||
const StorageAccessTile(),
|
||||
],
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue