mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
447 lines
13 KiB
Dart
447 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:kiwi/kiwi.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:nc_photos/app_init.dart' as app_init;
|
|
import 'package:nc_photos/app_localizations.dart';
|
|
import 'package:nc_photos/bloc/scan_local_dir.dart';
|
|
import 'package:nc_photos/di_container.dart';
|
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
|
import 'package:nc_photos/entity/local_file.dart';
|
|
import 'package:nc_photos/entity/pref.dart';
|
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
|
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';
|
|
import 'package:nc_photos/object_extension.dart';
|
|
import 'package:nc_photos/share_handler.dart';
|
|
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';
|
|
import 'package:nc_photos/widget/photo_list_item.dart';
|
|
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';
|
|
import 'package:np_async/np_async.dart';
|
|
import 'package:np_codegen/np_codegen.dart';
|
|
import 'package:np_collection/np_collection.dart';
|
|
import 'package:np_common/object_util.dart';
|
|
import 'package:np_platform_permission/np_platform_permission.dart';
|
|
import 'package:np_platform_util/np_platform_util.dart';
|
|
|
|
part 'enhanced_photo_browser.g.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;
|
|
}
|
|
|
|
@npLog
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
build(BuildContext context) {
|
|
return Scaffold(
|
|
body: BlocListener<ScanLocalDirBloc, ScanLocalDirBlocState>(
|
|
bloc: _bloc,
|
|
listener: (context, state) => _onStateChange(context, state),
|
|
child: BlocBuilder<ScanLocalDirBloc, ScanLocalDirBlocState>(
|
|
bloc: _bloc,
|
|
builder: (context, state) => _buildContent(context, state),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
onItemTap(SelectableItem item, int index) {
|
|
item.as<PhotoListLocalFileItem>()?.run((fileItem) {
|
|
Navigator.pushNamed(
|
|
context,
|
|
LocalFileViewer.routeName,
|
|
arguments: LocalFileViewerArguments(_backingFiles, fileItem.fileIndex),
|
|
);
|
|
});
|
|
}
|
|
|
|
void _initBloc() {
|
|
if (_bloc.state is ScanLocalDirBlocInit) {
|
|
_log.info("[_initBloc] Initialize bloc");
|
|
_reqQuery();
|
|
} else {
|
|
// process the current state
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
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 &&
|
|
!_buildItemQueue.isProcessing &&
|
|
itemStreamListItems.isEmpty) {
|
|
return Column(
|
|
children: [
|
|
AppBar(
|
|
title: Text(L10n.global().collectionEditedPhotosLabel),
|
|
elevation: 0,
|
|
),
|
|
Expanded(
|
|
child: EmptyListIndicator(
|
|
icon: Icons.folder_outlined,
|
|
text: L10n.global().listEmptyText,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
} else {
|
|
return Stack(
|
|
children: [
|
|
buildItemStreamListOuter(
|
|
context,
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
_buildAppBar(context),
|
|
buildItemStreamList(
|
|
maxCrossAxisExtent: _thumbSize.toDouble(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (state is ScanLocalDirBlocLoading || _buildItemQueue.isProcessing)
|
|
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),
|
|
);
|
|
|
|
Widget _buildSelectionAppBar(BuildContext context) {
|
|
return SelectionAppBar(
|
|
count: selectedListItems.length,
|
|
onClosePressed: () {
|
|
setState(() {
|
|
clearSelectedItems();
|
|
});
|
|
},
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.share),
|
|
tooltip: L10n.global().shareTooltip,
|
|
onPressed: () {
|
|
_onSelectionSharePressed(context);
|
|
},
|
|
),
|
|
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);
|
|
} 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,
|
|
));
|
|
}
|
|
}
|
|
|
|
Future<void> _onSelectionSharePressed(BuildContext context) async {
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
final selected = selectedListItems
|
|
.whereType<PhotoListLocalFileItem>()
|
|
.map((e) => e.file)
|
|
.toList();
|
|
await ShareHandler(
|
|
c,
|
|
context: context,
|
|
clearSelection: () {
|
|
setState(() {
|
|
clearSelectedItems();
|
|
});
|
|
},
|
|
).shareLocalFiles(selected);
|
|
}
|
|
|
|
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
|
|
.whereType<PhotoListLocalFileItem>()
|
|
.map((e) => e.file)
|
|
.toList();
|
|
setState(() {
|
|
clearSelectedItems();
|
|
});
|
|
await const DeleteLocalSelectionHandler()(selectedFiles: selectedFiles);
|
|
}
|
|
|
|
void _transformItems(
|
|
List<LocalFile> files, {
|
|
bool isSuccess = false,
|
|
}) {
|
|
_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!);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
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 (getRawPlatform() == NpPlatform.android) {
|
|
if (AndroidInfo().sdkInt >= AndroidVersion.R) {
|
|
if (!await Permission.hasReadExternalStorage()) {
|
|
final results = await requestReadExternalStorageForResult();
|
|
return results[Permission.READ_EXTERNAL_STORAGE] ==
|
|
PermissionRequestResult.granted ||
|
|
results[Permission.READ_MEDIA_IMAGES] ==
|
|
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;
|
|
}
|
|
|
|
void _reqQuery() {
|
|
_bloc.add(const ScanLocalDirBlocQuery([
|
|
"Download/Photos (for Nextcloud)/Enhanced Photos",
|
|
"Download/Photos (for Nextcloud)/Edited Photos",
|
|
]));
|
|
}
|
|
|
|
final _bloc = ScanLocalDirBloc();
|
|
|
|
var _backingFiles = <LocalFile>[];
|
|
|
|
final _buildItemQueue = ComputeQueue<_BuilderArguments, _BuilderResult>();
|
|
|
|
var _isFirstRun = true;
|
|
var _thumbZoomLevel = 0;
|
|
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
|
var _isNoPermission = false;
|
|
}
|
|
|
|
enum _SelectionMenuOption {
|
|
delete,
|
|
}
|
|
|
|
class _BuilderResult {
|
|
const _BuilderResult(this.backingFiles, this.listItems);
|
|
|
|
final List<LocalFile> backingFiles;
|
|
final List<SelectableItem> listItems;
|
|
}
|
|
|
|
class _BuilderArguments {
|
|
const _BuilderArguments(this.files);
|
|
|
|
final List<LocalFile> files;
|
|
}
|
|
|
|
@npLog
|
|
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");
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
_BuilderResult _fromSortedItems(List<LocalFile> files) {
|
|
final backingFiles = <LocalFile>[];
|
|
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);
|
|
listItems.add(item);
|
|
}
|
|
}
|
|
return _BuilderResult(backingFiles, listItems);
|
|
}
|
|
|
|
SelectableItem? _buildListItem(int i, LocalFile file) {
|
|
if (file_util.isSupportedImageMime(file.mime ?? "")) {
|
|
return PhotoListLocalImageItem(
|
|
fileIndex: i,
|
|
file: file,
|
|
);
|
|
} else {
|
|
_log.shout("[_buildListItem] Unsupported file format: ${file.mime}");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
_BuilderResult _buildPhotoListItem(_BuilderArguments arg) {
|
|
app_init.initLog();
|
|
return _Builder()(arg.files);
|
|
}
|