Show state of the metadata task in photos page

This commit is contained in:
Ming Ming 2021-08-16 01:06:00 +08:00
parent 31452bc5fb
commit 1e941e8a05
9 changed files with 262 additions and 28 deletions

View file

@ -1,11 +1,13 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
Future<void> waitUntilWifi() async {
Future<void> waitUntilWifi({VoidCallback? onNoWifi}) async {
while (true) {
final result = await Connectivity().checkConnectivity();
if (result == ConnectivityResult.wifi) {
return;
}
onNoWifi?.call();
await Future.delayed(const Duration(seconds: 5));
}
}

View file

@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/metadata_task_manager.dart';
class AppEventListener<T> {
AppEventListener(this._listener);
@ -87,6 +88,12 @@ class ThemeChangedEvent {}
class LanguageChangedEvent {}
class MetadataTaskStateChangedEvent {
const MetadataTaskStateChangedEvent(this.state);
final MetadataTaskState state;
}
extension FilePropertyUpdatedEventExtension on FilePropertyUpdatedEvent {
bool hasAnyProperties(List<int> properties) =>
properties.any((p) => this.properties & p != 0);

View file

@ -643,6 +643,15 @@
"@setAlbumCoverFailureNotification": {
"description": "Cannot set the opened item as the album cover"
},
"metadataTaskProcessingNotification": "Processing image metadata in background",
"@metadataTaskProcessingNotification": {
"description": "Shown when the app is reading image metadata"
},
"metadataTaskPauseNoWiFiNotification": "Waiting for WiFi",
"@metadataTaskPauseNoWiFiNotification": {
"description": "Shown when the app has paused reading image metadata"
},
"configButtonLabel": "CONFIG",
"changelogTitle": "Changelog",
"@changelogTitle": {

View file

@ -15,14 +15,20 @@
"albumSharedLabel",
"setAlbumCoverProcessingNotification",
"setAlbumCoverSuccessNotification",
"setAlbumCoverFailureNotification"
"setAlbumCoverFailureNotification",
"metadataTaskProcessingNotification",
"metadataTaskPauseNoWiFiNotification",
"configButtonLabel"
],
"es": [
"albumSharedLabel",
"setAlbumCoverProcessingNotification",
"setAlbumCoverSuccessNotification",
"setAlbumCoverFailureNotification"
"setAlbumCoverFailureNotification",
"metadataTaskProcessingNotification",
"metadataTaskPauseNoWiFiNotification",
"configButtonLabel"
],
"fr": [
@ -41,6 +47,9 @@
"albumSharedLabel",
"setAlbumCoverProcessingNotification",
"setAlbumCoverSuccessNotification",
"setAlbumCoverFailureNotification"
"setAlbumCoverFailureNotification",
"metadataTaskProcessingNotification",
"metadataTaskPauseNoWiFiNotification",
"configButtonLabel"
]
}

View file

@ -7,7 +7,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/metadata_task_manager.dart';
import 'package:nc_photos/mobile/self_signed_cert_manager.dart';
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/pref.dart';
@ -90,7 +89,6 @@ void _initBloc() {
void _initKiwi() {
final kiwi = KiwiContainer();
kiwi.registerInstance<EventBus>(EventBus());
kiwi.registerInstance<MetadataTaskManager>(MetadataTaskManager());
}
void _initEquatable() {

View file

@ -1,10 +1,13 @@
import 'dart:async';
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/use_case/update_missing_metadata.dart';
@ -20,17 +23,23 @@ class MetadataTask {
}
Future<void> call() async {
final fileRepo = FileRepo(FileCachedDataSource());
for (final r in account.roots) {
final op = UpdateMissingMetadata(fileRepo);
await for (final _ in op(account,
File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) {
if (!Pref.inst().isEnableExifOr()) {
_log.info("[call] EXIF disabled, task ending immaturely");
op.stop();
return;
try {
final fileRepo = FileRepo(FileCachedDataSource());
for (final r in account.roots) {
final op = UpdateMissingMetadata(fileRepo);
await for (final _ in op(account,
File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) {
if (!Pref.inst().isEnableExifOr()) {
_log.info("[call] EXIF disabled, task ending immaturely");
op.stop();
return;
}
}
}
} finally {
KiwiContainer()
.resolve<EventBus>()
.fire(MetadataTaskStateChangedEvent(MetadataTaskState.idle));
}
}
@ -41,7 +50,15 @@ class MetadataTask {
/// Manage metadata tasks to run concurrently
class MetadataTaskManager {
MetadataTaskManager() {
factory MetadataTaskManager() {
if (_inst == null) {
_inst = MetadataTaskManager._();
}
return _inst!;
}
MetadataTaskManager._() {
_stateChangedListener.begin();
_handleStream();
}
@ -51,6 +68,14 @@ class MetadataTaskManager {
_streamController.add(task);
}
MetadataTaskState get state => _currentState;
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
if (ev.state != _currentState) {
_currentState = ev.state;
}
}
void _handleStream() async {
await for (final task in _streamController.stream) {
if (Pref.inst().isEnableExifOr()) {
@ -64,5 +89,23 @@ class MetadataTaskManager {
final _streamController = StreamController<MetadataTask>.broadcast();
var _currentState = MetadataTaskState.idle;
late final _stateChangedListener =
AppEventListener<MetadataTaskStateChangedEvent>(
_onMetadataTaskStateChanged);
static final _log = Logger("metadata_task_manager.MetadataTaskManager");
static MetadataTaskManager? _inst;
}
enum MetadataTaskState {
/// No work is being done
idle,
/// Processing images
prcoessing,
/// Paused on data network
waitingForWifi,
}

View file

@ -1,9 +1,13 @@
import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/connectivity_util.dart' as connectivity_util;
import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/metadata_task_manager.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/use_case/get_file_binary.dart';
import 'package:nc_photos/use_case/load_metadata.dart';
@ -33,7 +37,13 @@ class UpdateMissingMetadata {
try {
// since we need to download multiple images in their original size,
// we only do it with WiFi
await connectivity_util.waitUntilWifi();
await connectivity_util.waitUntilWifi(onNoWifi: () {
KiwiContainer().resolve<EventBus>().fire(
MetadataTaskStateChangedEvent(MetadataTaskState.waitingForWifi));
});
KiwiContainer()
.resolve<EventBus>()
.fire(MetadataTaskStateChangedEvent(MetadataTaskState.prcoessing));
if (!shouldRun) {
return;
}

View file

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -17,6 +19,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.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;
@ -37,6 +40,7 @@ import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:nc_photos/widget/photo_list_item.dart';
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
import 'package:nc_photos/widget/selection_app_bar.dart';
import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/viewer.dart';
import 'package:nc_photos/widget/zoom_menu_button.dart';
@ -56,12 +60,21 @@ class _HomePhotosState extends State<HomePhotos>
with
SelectableItemStreamListMixin<HomePhotos>,
RouteAware,
PageVisibilityMixin {
PageVisibilityMixin,
TickerProviderStateMixin {
@override
initState() {
super.initState();
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevelOr(0);
_initBloc();
_metadataTaskStateChangedListener.begin();
}
@override
dispose() {
_metadataTaskIconController.stop();
_metadataTaskStateChangedListener.end();
super.dispose();
}
@override
@ -112,6 +125,8 @@ class _HomePhotosState extends State<HomePhotos>
controller: _scrollController,
slivers: [
_buildAppBar(context),
if (_metadataTaskState != MetadataTaskState.idle)
_buildMetadataTaskHeader(context),
SliverPadding(
padding: const EdgeInsets.symmetric(vertical: 8),
sliver: buildItemStreamList(
@ -228,6 +243,80 @@ class _HomePhotosState extends State<HomePhotos>
);
}
Widget _buildMetadataTaskHeader(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _MetadataTaskHeaderDelegate(
extent: _metadataTaskHeaderHeight,
builder: (context) => Container(
height: double.infinity,
color: Theme.of(context).scaffoldBackgroundColor,
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
if (_metadataTaskState == MetadataTaskState.prcoessing)
Row(
mainAxisSize: MainAxisSize.min,
children: [
_MetadataTaskLoadingIcon(
controller: _metadataTaskIconController,
),
const SizedBox(width: 4),
Text(
L10n.of(context).metadataTaskProcessingNotification,
style: TextStyle(fontSize: 12),
),
],
)
else if (_metadataTaskState == MetadataTaskState.waitingForWifi)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.sync_problem,
size: 16,
),
const SizedBox(width: 4),
Text(
L10n.of(context).metadataTaskPauseNoWiFiNotification,
style: TextStyle(fontSize: 12),
),
],
),
Expanded(
child: Container(),
),
Material(
type: MaterialType.transparency,
child: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Text(
L10n.of(context).configButtonLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 12,
),
),
),
onTap: () {
Navigator.of(context).pushNamed(Settings.routeName,
arguments: SettingsArguments(widget.account));
},
),
),
],
),
),
),
),
);
}
void _onStateChange(BuildContext context, ScanDirBlocState state) {
if (state is ScanDirBlocInit) {
itemStreamListItems = [];
@ -235,9 +324,7 @@ class _HomePhotosState extends State<HomePhotos>
_transformItems(state.files);
if (state is ScanDirBlocSuccess) {
if (Pref.inst().isEnableExifOr() && !_hasFiredMetadataTask.value) {
KiwiContainer()
.resolve<MetadataTaskManager>()
.addTask(MetadataTask(widget.account));
MetadataTaskManager().addTask(MetadataTask(widget.account));
_hasFiredMetadataTask.value = true;
}
}
@ -451,6 +538,14 @@ class _HomePhotosState extends State<HomePhotos>
_reqRefresh();
}
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
if (ev.state != _metadataTaskState) {
setState(() {
_metadataTaskState = ev.state;
});
}
}
/// Transform a File list to grid items
void _transformItems(List<File> files) {
_backingFiles = files
@ -530,11 +625,18 @@ class _HomePhotosState extends State<HomePhotos>
if (_itemListMaxExtent != null &&
constraints.hasBoundedHeight &&
_appBarExtent != null) {
// scroll extent = list height - widget viewport height + sliver app bar height + list padding
final scrollExtent =
_itemListMaxExtent! - constraints.maxHeight + _appBarExtent! + 16;
final metadataTaskHeaderExtent =
_metadataTaskState == MetadataTaskState.idle
? 0
: _metadataTaskHeaderHeight;
// scroll extent = list height - widget viewport height + sliver app bar height + metadata task header height + list padding
final scrollExtent = _itemListMaxExtent! -
constraints.maxHeight +
_appBarExtent! +
metadataTaskHeaderExtent +
16;
_log.info(
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + 16 = $scrollExtent");
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + $metadataTaskHeaderExtent + 16 = $scrollExtent");
return scrollExtent;
} else {
return null;
@ -586,8 +688,20 @@ class _HomePhotosState extends State<HomePhotos>
double? _appBarExtent;
double? _itemListMaxExtent;
late final _metadataTaskStateChangedListener =
AppEventListener<MetadataTaskStateChangedEvent>(
_onMetadataTaskStateChanged);
var _metadataTaskState = MetadataTaskManager().state;
late final _metadataTaskIconController = AnimationController(
upperBound: 2 * math.pi,
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
static final _log = Logger("widget.home_photos._HomePhotosState");
static const _menuValueRefresh = 0;
static const _metadataTaskHeaderHeight = 32.0;
}
abstract class _ListItem implements SelectableItem {
@ -706,6 +820,50 @@ class _VideoListItem extends _FileListItem {
final String previewUrl;
}
class _MetadataTaskHeaderDelegate extends SliverPersistentHeaderDelegate {
const _MetadataTaskHeaderDelegate({
required this.extent,
required this.builder,
});
@override
build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return builder(context);
}
@override
get maxExtent => extent;
@override
get minExtent => maxExtent;
@override
shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
final double extent;
final Widget Function(BuildContext context) builder;
}
class _MetadataTaskLoadingIcon extends AnimatedWidget {
const _MetadataTaskLoadingIcon({
Key? key,
required AnimationController controller,
}) : super(key: key, listenable: controller);
@override
build(BuildContext context) {
return Transform.rotate(
angle: -_progress.value,
child: const Icon(
Icons.sync,
size: 16,
),
);
}
Animation<double> get _progress => listenable as Animation<double>;
}
enum _SelectionAppBarMenuOption {
archive,
delete,

View file

@ -217,9 +217,7 @@ class _SettingsState extends State<Settings> {
Pref.inst().setEnableExif(value).then((result) {
if (result) {
if (value) {
KiwiContainer()
.resolve<MetadataTaskManager>()
.addTask(MetadataTask(widget.account));
MetadataTaskManager().addTask(MetadataTask(widget.account));
}
} else {
_log.severe("[_setExifSupport] Failed writing pref");