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

446 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:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/scan_local_dir.dart';
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';
2022-05-06 11:16:56 +02:00
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
import 'package:nc_photos/mobile/android/permission_util.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/theme.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_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) {
return AppTheme(
child: 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),
),
),
),
);
}
void _initBloc() {
if (_bloc.state is ScanLocalDirBlocInit) {
_log.info("[_initBloc] Initialize bloc");
_reqQuery();
} else {
// process the current state
WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() {
_onStateChange(context, _bloc.state);
});
_reqQuery();
});
}
}
Widget _buildContent(BuildContext context, ScanLocalDirBlocState state) {
if (_isNoPermission) {
return Column(
children: [
AppBar(
title: Text(L10n.global().collectionEnhancedPhotosLabel),
elevation: 0,
),
Expanded(
child: EmptyListIndicator(
icon: Icons.folder_off_outlined,
text: L10n.global().errorNoStoragePermission,
),
),
],
);
} else if (state is ScanLocalDirBlocSuccess &&
itemStreamListItems.isEmpty) {
2022-05-06 11:16:56 +02:00
return Column(
children: [
AppBar(
title: Text(L10n.global().collectionEnhancedPhotosLabel),
elevation: 0,
),
Expanded(
child: EmptyListIndicator(
icon: Icons.folder_outlined,
text: L10n.global().listEmptyText,
),
),
],
);
} else {
return Stack(
children: [
buildItemStreamListOuter(
context,
child: Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(
secondary: AppTheme.getOverscrollIndicatorColor(context),
),
),
child: CustomScrollView(
slivers: [
_buildAppBar(context),
buildItemStreamList(
maxCrossAxisExtent: _thumbSize.toDouble(),
),
],
),
),
),
if (state is ScanLocalDirBlocLoading)
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().collectionEnhancedPhotosLabel),
);
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);
if (_isFirstRun) {
_isFirstRun = false;
if (widget.filename != null) {
_openInitialImage(widget.filename!);
}
}
} 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 selected = selectedListItems
.whereType<_FileListItem>()
.map((e) => e.file)
.toList();
await ShareHandler(
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
.whereType<_FileListItem>()
.map((e) => e.file)
.toList();
setState(() {
clearSelectedItems();
});
await const DeleteLocalSelectionHandler()(selectedFiles: selectedFiles);
}
void _onItemTap(int index) {
Navigator.pushNamed(context, LocalFileViewer.routeName,
arguments: LocalFileViewerArguments(_backingFiles, index));
}
void _transformItems(List<LocalFile> files) {
// we use last modified here to keep newly enhanced photo at the top
_backingFiles =
files.stableSorted((a, b) => b.lastModified.compareTo(a.lastModified));
itemStreamListItems = () sync* {
for (int i = 0; i < _backingFiles.length; ++i) {
final f = _backingFiles[i];
if (file_util.isSupportedImageMime(f.mime ?? "")) {
yield _ImageListItem(
file: f,
onTap: () => _onItemTap(i),
);
}
}
}()
.toList();
_log.info("[_transformItems] Length: ${itemStreamListItems.length}");
}
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"]));
}
final _bloc = ScanLocalDirBloc();
var _backingFiles = <LocalFile>[];
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");
}
abstract class _ListItem implements SelectableItem {
_ListItem({
VoidCallback? onTap,
}) : _onTap = onTap;
@override
get onTap => _onTap;
@override
get isSelectable => true;
@override
get staggeredTile => const StaggeredTile.count(1, 1);
final VoidCallback? _onTap;
}
abstract class _FileListItem extends _ListItem {
_FileListItem({
required this.file,
VoidCallback? onTap,
}) : super(onTap: onTap);
final LocalFile file;
}
class _ImageListItem extends _FileListItem {
_ImageListItem({
required LocalFile file,
VoidCallback? onTap,
}) : super(file: file, onTap: onTap);
@override
buildWidget(BuildContext context) {
final ImageProvider provider;
if (file is LocalUriFile) {
provider = ContentUriImage((file as LocalUriFile).uri);
} else {
throw ArgumentError("Invalid file");
}
return Padding(
padding: const EdgeInsets.all(2),
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: Container(
// arbitrary size here
constraints: BoxConstraints.tight(const Size(128, 128)),
color: AppTheme.getListItemBackgroundColor(context),
child: Image(
image: ResizeImage.resizeIfNeeded(k.photoThumbSize, null, provider),
filterQuality: FilterQuality.high,
fit: BoxFit.cover,
errorBuilder: (context, e, stackTrace) {
return Center(
child: Icon(
Icons.image_not_supported,
size: 64,
color: Colors.white.withOpacity(.8),
),
);
},
),
),
),
);
}
}
enum _SelectionMenuOption {
delete,
}