mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-29 02:11:37 +01:00
Show state of the metadata task in photos page
This commit is contained in:
parent
31452bc5fb
commit
1e941e8a05
9 changed files with 262 additions and 28 deletions
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Reference in a new issue