nc-photos/app/lib/widget/enhanced_photo_browser.dart

444 lines
13 KiB
Dart
Raw Normal View History

2022-05-06 11:16:56 +02:00
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
2022-05-06 11:16:56 +02:00
import 'package:logging/logging.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/app_init.dart' as app_init;
2022-05-06 11:16:56 +02:00
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/scan_local_dir.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart';
2022-05-06 11:16:56 +02:00
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/android_info.dart';
import 'package:nc_photos/mobile/android/permission_util.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
2022-05-06 11:16:56 +02:00
import 'package:nc_photos/pref.dart';
2022-05-07 12:27:26 +02:00
import 'package:nc_photos/share_handler.dart';
2022-05-06 11:16:56 +02:00
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/widget/empty_list_indicator.dart';
import 'package:nc_photos/widget/handler/delete_local_selection_handler.dart';
import 'package:nc_photos/widget/local_file_viewer.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/widget/photo_list_item.dart';
2022-05-06 11:16:56 +02:00
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
import 'package:nc_photos/widget/selection_app_bar.dart';
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
class EnhancedPhotoBrowserArguments {
const EnhancedPhotoBrowserArguments(this.filename);
final String? filename;
}
class EnhancedPhotoBrowser extends StatefulWidget {
static const routeName = "/enhanced-photo-browser";
static Route buildRoute(EnhancedPhotoBrowserArguments args) =>
MaterialPageRoute(
builder: (context) => EnhancedPhotoBrowser.fromArgs(args),
);
const EnhancedPhotoBrowser({
Key? key,
required this.filename,
}) : super(key: key);
EnhancedPhotoBrowser.fromArgs(EnhancedPhotoBrowserArguments args, {Key? key})
: this(
key: key,
filename: args.filename,
);
@override
createState() => _EnhancedPhotoBrowserState();
final String? filename;
}
class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
with SelectableItemStreamListMixin<EnhancedPhotoBrowser> {
@override
initState() {
super.initState();
_thumbZoomLevel = Pref().getAlbumBrowserZoomLevelOr(0);
_ensurePermission().then((value) {
if (value) {
_initBloc();
} else {
if (mounted) {
setState(() {
_isNoPermission = true;
});
}
}
});
2022-05-06 11:16:56 +02:00
}
@override
build(BuildContext context) {
2022-11-12 10:55:33 +01:00
return Scaffold(
body: BlocListener<ScanLocalDirBloc, ScanLocalDirBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ScanLocalDirBloc, ScanLocalDirBlocState>(
2022-05-06 11:16:56 +02:00
bloc: _bloc,
2022-11-12 10:55:33 +01:00
builder: (context, state) => _buildContent(context, state),
2022-05-06 11:16:56 +02:00
),
),
);
}
2022-06-06 19:37:46 +02:00
@override
onItemTap(SelectableItem item, int index) {
item.as<PhotoListLocalFileItem>()?.run((fileItem) {
Navigator.pushNamed(
context,
LocalFileViewer.routeName,
arguments: LocalFileViewerArguments(_backingFiles, fileItem.fileIndex),
);
});
}
2022-05-06 11:16:56 +02:00
void _initBloc() {
if (_bloc.state is ScanLocalDirBlocInit) {
_log.info("[_initBloc] Initialize bloc");
_reqQuery();
} else {
// process the current state
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2022-05-06 11:16:56 +02:00
setState(() {
_onStateChange(context, _bloc.state);
});
_reqQuery();
});
}
}
Widget _buildContent(BuildContext context, ScanLocalDirBlocState state) {
if (_isNoPermission) {
return Column(
children: [
AppBar(
title: Text(L10n.global().collectionEditedPhotosLabel),
elevation: 0,
),
Expanded(
child: EmptyListIndicator(
icon: Icons.folder_off_outlined,
text: L10n.global().errorNoStoragePermission,
),
),
],
);
} else if (state is ScanLocalDirBlocSuccess &&
2022-06-06 19:37:46 +02:00
!_buildItemQueue.isProcessing &&
itemStreamListItems.isEmpty) {
2022-05-06 11:16:56 +02:00
return Column(
children: [
AppBar(
title: Text(L10n.global().collectionEditedPhotosLabel),
2022-05-06 11:16:56 +02:00
elevation: 0,
),
Expanded(
child: EmptyListIndicator(
icon: Icons.folder_outlined,
text: L10n.global().listEmptyText,
),
),
],
);
} else {
return Stack(
children: [
buildItemStreamListOuter(
context,
2022-11-12 10:55:33 +01:00
child: CustomScrollView(
slivers: [
_buildAppBar(context),
buildItemStreamList(
maxCrossAxisExtent: _thumbSize.toDouble(),
),
],
2022-05-06 11:16:56 +02:00
),
),
2022-06-06 19:37:46 +02:00
if (state is ScanLocalDirBlocLoading || _buildItemQueue.isProcessing)
2022-05-06 11:16:56 +02:00
const Align(
alignment: Alignment.bottomCenter,
child: LinearProgressIndicator(),
),
],
);
}
}
Widget _buildAppBar(BuildContext context) {
if (isSelectionMode) {
return _buildSelectionAppBar(context);
} else {
return _buildNormalAppBar(context);
}
}
Widget _buildNormalAppBar(BuildContext context) => SliverAppBar(
title: Text(L10n.global().collectionEditedPhotosLabel),
2022-05-06 11:16:56 +02:00
);
Widget _buildSelectionAppBar(BuildContext context) {
return SelectionAppBar(
count: selectedListItems.length,
onClosePressed: () {
setState(() {
clearSelectedItems();
});
},
actions: [
2022-05-07 12:27:26 +02:00
IconButton(
icon: const Icon(Icons.share),
tooltip: L10n.global().shareTooltip,
onPressed: () {
_onSelectionSharePressed(context);
},
),
2022-05-06 11:16:56 +02:00
PopupMenuButton<_SelectionMenuOption>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) => [
PopupMenuItem(
value: _SelectionMenuOption.delete,
child: Text(L10n.global().deletePermanentlyTooltip),
),
],
onSelected: (option) => _onSelectionMenuSelected(context, option),
),
],
);
}
void _onStateChange(BuildContext context, ScanLocalDirBlocState state) {
if (state is ScanLocalDirBlocInit) {
itemStreamListItems = [];
} else if (state is ScanLocalDirBlocLoading) {
_transformItems(state.files);
} else if (state is ScanLocalDirBlocSuccess) {
_transformItems(state.files, isSuccess: true);
2022-05-06 11:16:56 +02:00
} else if (state is ScanLocalDirBlocFailure) {
_transformItems(state.files);
SnackBarManager().showSnackBar(SnackBar(
content: Text(state.exception is PermissionException
? L10n.global().errorNoStoragePermission
: exception_util.toUserString(state.exception)),
duration: k.snackBarDurationNormal,
));
}
}
2022-05-07 12:27:26 +02:00
Future<void> _onSelectionSharePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
2022-05-07 12:27:26 +02:00
final selected = selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListLocalFileItem>()
2022-05-07 12:27:26 +02:00
.map((e) => e.file)
.toList();
await ShareHandler(
c,
2022-05-07 12:27:26 +02:00
context: context,
clearSelection: () {
setState(() {
clearSelectedItems();
});
},
).shareLocalFiles(selected);
}
2022-05-06 11:16:56 +02:00
void _onSelectionMenuSelected(
BuildContext context, _SelectionMenuOption option) {
switch (option) {
case _SelectionMenuOption.delete:
_onSelectionDeletePressed(context);
break;
default:
_log.shout("[_onSelectionMenuSelected] Unknown option: $option");
break;
}
}
Future<void> _onSelectionDeletePressed(BuildContext context) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(L10n.global().deletePermanentlyConfirmationDialogTitle),
content: Text(
L10n.global().deletePermanentlyLocalConfirmationDialogContent,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(L10n.global().confirmButtonLabel),
),
],
),
);
if (result != true) {
return;
}
final selectedFiles = selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListLocalFileItem>()
2022-05-06 11:16:56 +02:00
.map((e) => e.file)
.toList();
setState(() {
clearSelectedItems();
});
await const DeleteLocalSelectionHandler()(selectedFiles: selectedFiles);
}
void _transformItems(
List<LocalFile> files, {
bool isSuccess = false,
}) {
2022-06-06 19:37:46 +02:00
_buildItemQueue.addJob(
_BuilderArguments(files),
_buildPhotoListItem,
(result) {
if (mounted) {
setState(() {
_backingFiles = result.backingFiles;
itemStreamListItems = result.listItems;
});
if (isSuccess && _isFirstRun) {
_isFirstRun = false;
if (widget.filename != null) {
_openInitialImage(widget.filename!);
}
}
}
2022-06-06 19:37:46 +02:00
},
);
2022-05-06 11:16:56 +02:00
}
void _openInitialImage(String filename) {
final index = _backingFiles.indexWhere((f) => f.filename == filename);
if (index == -1) {
_log.severe("[openInitialImage] Filename not found: $filename");
return;
}
Navigator.pushNamed(context, LocalFileViewer.routeName,
arguments: LocalFileViewerArguments(_backingFiles, index));
}
Future<bool> _ensurePermission() async {
if (platform_k.isAndroid) {
if (AndroidInfo().sdkInt >= AndroidVersion.R) {
if (!await Permission.hasReadExternalStorage()) {
final results = await requestPermissionsForResult([
Permission.READ_EXTERNAL_STORAGE,
]);
return results[Permission.READ_EXTERNAL_STORAGE] ==
PermissionRequestResult.granted;
}
} else {
if (!await Permission.hasWriteExternalStorage()) {
final results = await requestPermissionsForResult([
Permission.WRITE_EXTERNAL_STORAGE,
]);
return results[Permission.WRITE_EXTERNAL_STORAGE] ==
PermissionRequestResult.granted;
}
}
}
return true;
}
2022-05-06 11:16:56 +02:00
void _reqQuery() {
_bloc.add(const ScanLocalDirBlocQuery([
"Download/Photos (for Nextcloud)/Enhanced Photos",
"Download/Photos (for Nextcloud)/Edited Photos",
]));
2022-05-06 11:16:56 +02:00
}
final _bloc = ScanLocalDirBloc();
var _backingFiles = <LocalFile>[];
2022-06-06 19:37:46 +02:00
final _buildItemQueue = ComputeQueue<_BuilderArguments, _BuilderResult>();
2022-05-06 11:16:56 +02:00
var _isFirstRun = true;
var _thumbZoomLevel = 0;
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
var _isNoPermission = false;
2022-05-06 11:16:56 +02:00
static final _log =
Logger("widget.enhanced_photo_browser._EnhancedPhotoBrowserState");
}
2022-06-06 19:37:46 +02:00
enum _SelectionMenuOption {
delete,
}
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
class _BuilderResult {
const _BuilderResult(this.backingFiles, this.listItems);
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
final List<LocalFile> backingFiles;
final List<SelectableItem> listItems;
}
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
class _BuilderArguments {
const _BuilderArguments(this.files);
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
final List<LocalFile> files;
2022-05-06 11:16:56 +02:00
}
2022-06-06 19:37:46 +02:00
class _Builder {
_BuilderResult call(List<LocalFile> files) {
final s = Stopwatch()..start();
try {
return _fromSortedItems(_sortItems(files));
} finally {
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
}
}
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
List<LocalFile> _sortItems(List<LocalFile> files) {
// we use last modified here to keep newly enhanced photo at the top
return files
.stableSorted((a, b) => b.lastModified.compareTo(a.lastModified));
}
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
_BuilderResult _fromSortedItems(List<LocalFile> files) {
final backingFiles = <LocalFile>[];
2022-06-06 19:37:46 +02:00
final listItems = <SelectableItem>[];
for (int i = 0; i < files.length; ++i) {
final f = files[i];
final item = _buildListItem(i, f);
if (item != null) {
backingFiles.add(f);
2022-06-06 19:37:46 +02:00
listItems.add(item);
}
}
return _BuilderResult(backingFiles, listItems);
2022-06-06 19:37:46 +02:00
}
2022-05-06 11:16:56 +02:00
2022-06-06 19:37:46 +02:00
SelectableItem? _buildListItem(int i, LocalFile file) {
if (file_util.isSupportedImageMime(file.mime ?? "")) {
return PhotoListLocalImageItem(
fileIndex: i,
file: file,
);
2022-05-06 11:16:56 +02:00
} else {
2022-06-06 19:37:46 +02:00
_log.shout("[_buildListItem] Unsupported file format: ${file.mime}");
return null;
2022-05-06 11:16:56 +02:00
}
}
2022-06-06 19:37:46 +02:00
static final _log = Logger("widget.enhanced_photo_browser._Builder");
2022-05-06 11:16:56 +02:00
}
2022-06-06 19:37:46 +02:00
_BuilderResult _buildPhotoListItem(_BuilderArguments arg) {
app_init.initLog();
return _Builder()(arg.files);
2022-05-06 11:16:56 +02:00
}