mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Run metadata task in a service on Android
This commit is contained in:
parent
996bebfdd2
commit
3f8832c0e1
9 changed files with 571 additions and 129 deletions
|
@ -12,7 +12,9 @@ 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/event/native_event.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/throttler.dart';
|
||||
import 'package:nc_photos/touch_token_manager.dart';
|
||||
|
@ -112,6 +114,8 @@ class ScanAccountDirBloc
|
|||
_favoriteResyncedEventListener.begin();
|
||||
_prefUpdatedEventListener.begin();
|
||||
_accountPrefUpdatedEventListener.begin();
|
||||
|
||||
_nativeFileExifUpdatedListener?.begin();
|
||||
}
|
||||
|
||||
static ScanAccountDirBloc of(Account account) {
|
||||
|
@ -164,6 +168,8 @@ class ScanAccountDirBloc
|
|||
_prefUpdatedEventListener.end();
|
||||
_accountPrefUpdatedEventListener.end();
|
||||
|
||||
_nativeFileExifUpdatedListener?.end();
|
||||
|
||||
_refreshThrottler.clear();
|
||||
return super.close();
|
||||
}
|
||||
|
@ -304,6 +310,13 @@ class ScanAccountDirBloc
|
|||
}
|
||||
}
|
||||
|
||||
void _onNativeFileExifUpdated(FileExifUpdatedEvent ev) {
|
||||
_refreshThrottler.trigger(
|
||||
maxResponceTime: const Duration(seconds: 3),
|
||||
maxPendingCount: 10,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<File>> _queryOffline(ScanAccountDirBlocQueryBase ev) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final files = <File>[];
|
||||
|
@ -484,6 +497,10 @@ class ScanAccountDirBloc
|
|||
late final _accountPrefUpdatedEventListener =
|
||||
AppEventListener<AccountPrefUpdatedEvent>(_onAccountPrefUpdatedEvent);
|
||||
|
||||
late final _nativeFileExifUpdatedListener = platform_k.isWeb
|
||||
? null
|
||||
: NativeEventListener<FileExifUpdatedEvent>(_onNativeFileExifUpdated);
|
||||
|
||||
late final _refreshThrottler = Throttler(
|
||||
onTriggered: (_) {
|
||||
add(const _ScanAccountDirBlocExternalEvent());
|
||||
|
|
67
app/lib/event/native_event.dart
Normal file
67
app/lib/event/native_event.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||
|
||||
class NativeEventListener<T> {
|
||||
NativeEventListener(this.listener);
|
||||
|
||||
void begin() {
|
||||
if (_subscription != null) {
|
||||
_log.warning("[begin] Already listening");
|
||||
return;
|
||||
}
|
||||
_subscription = _mappedStream
|
||||
.where((event) => event is T)
|
||||
.cast<T>()
|
||||
.listen(listener);
|
||||
}
|
||||
|
||||
void end() {
|
||||
if (_subscription == null) {
|
||||
_log.warning("[end] Already not listening");
|
||||
return;
|
||||
}
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
|
||||
static late final _mappedStream =
|
||||
NativeEvent.stream.where((event) => event is NativeEventObject).map((ev) {
|
||||
switch (ev.event) {
|
||||
case FileExifUpdatedEvent._id:
|
||||
return FileExifUpdatedEvent.fromEvent(ev);
|
||||
|
||||
default:
|
||||
throw ArgumentError("Invalid event: ${ev.event}");
|
||||
}
|
||||
});
|
||||
|
||||
final void Function(T) listener;
|
||||
StreamSubscription<T>? _subscription;
|
||||
|
||||
final _log = Logger("native_event.NativeEventListener<${T.runtimeType}>");
|
||||
}
|
||||
|
||||
class FileExifUpdatedEvent {
|
||||
const FileExifUpdatedEvent(this.fileIds);
|
||||
|
||||
factory FileExifUpdatedEvent.fromEvent(NativeEventObject ev) {
|
||||
assert(ev.event == _id);
|
||||
assert(ev.data != null);
|
||||
final dataJson = jsonDecode(ev.data!) as Map;
|
||||
return FileExifUpdatedEvent((dataJson["fileIds"] as List).cast<int>());
|
||||
}
|
||||
|
||||
NativeEventObject toEvent() => NativeEventObject(
|
||||
_id,
|
||||
jsonEncode({
|
||||
"fileIds": fileIds,
|
||||
}),
|
||||
);
|
||||
|
||||
static const _id = "FileExifUpdatedEvent";
|
||||
|
||||
final List<int> fileIds;
|
||||
}
|
|
@ -1167,6 +1167,10 @@
|
|||
"@tagPickerNoTagSelectedNotification": {
|
||||
"description": "At least 1 tag is required to create a tag collection. This error is shown when user try to create one without selecting any tags"
|
||||
},
|
||||
"backgroundServiceStopping": "Stopping service",
|
||||
"@backgroundServiceStopping": {
|
||||
"description": "The background service is stopping itself"
|
||||
},
|
||||
|
||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||
"@errorUnauthenticated": {
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"createCollectionDialogTagDescription",
|
||||
"addTagInputHint",
|
||||
"tagPickerNoTagSelectedNotification",
|
||||
"backgroundServiceStopping",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -182,6 +183,7 @@
|
|||
"createCollectionDialogTagDescription",
|
||||
"addTagInputHint",
|
||||
"tagPickerNoTagSelectedNotification",
|
||||
"backgroundServiceStopping",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -337,12 +339,18 @@
|
|||
"createCollectionDialogTagDescription",
|
||||
"addTagInputHint",
|
||||
"tagPickerNoTagSelectedNotification",
|
||||
"backgroundServiceStopping",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"rootPickerSkipConfirmationDialogContent2",
|
||||
"helpButtonLabel"
|
||||
"helpButtonLabel",
|
||||
"backgroundServiceStopping"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"backgroundServiceStopping"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
|
@ -477,6 +485,7 @@
|
|||
"createCollectionDialogTagDescription",
|
||||
"addTagInputHint",
|
||||
"tagPickerNoTagSelectedNotification",
|
||||
"backgroundServiceStopping",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -499,7 +508,8 @@
|
|||
"createCollectionDialogTagLabel",
|
||||
"createCollectionDialogTagDescription",
|
||||
"addTagInputHint",
|
||||
"tagPickerNoTagSelectedNotification"
|
||||
"tagPickerNoTagSelectedNotification",
|
||||
"backgroundServiceStopping"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -607,6 +617,7 @@
|
|||
"createCollectionDialogTagDescription",
|
||||
"addTagInputHint",
|
||||
"tagPickerNoTagSelectedNotification",
|
||||
"backgroundServiceStopping",
|
||||
"errorAlbumDowngrade"
|
||||
]
|
||||
}
|
||||
|
|
266
app/lib/service.dart
Normal file
266
app/lib/service.dart
Normal file
|
@ -0,0 +1,266 @@
|
|||
import 'package:devicelocale/devicelocale.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_init.dart' as app_init;
|
||||
import 'package:nc_photos/app_localizations.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/event/native_event.dart';
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||
|
||||
/// Start the background service
|
||||
Future<void> startService() async {
|
||||
_log.info("[startService] Starting service");
|
||||
final service = FlutterBackgroundService();
|
||||
await service.configure(
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
onStart: serviceMain,
|
||||
autoStart: false,
|
||||
isForegroundMode: true,
|
||||
foregroundServiceNotificationTitle:
|
||||
L10n.global().metadataTaskProcessingNotification,
|
||||
),
|
||||
iosConfiguration: IosConfiguration(
|
||||
onForeground: () => throw UnimplementedError(),
|
||||
onBackground: () => throw UnimplementedError(),
|
||||
),
|
||||
);
|
||||
await service.start();
|
||||
}
|
||||
|
||||
/// Ask the background service to stop ASAP
|
||||
void stopService() {
|
||||
_log.info("[stopService] Stopping service");
|
||||
FlutterBackgroundService().sendData({
|
||||
"stop": true,
|
||||
});
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void serviceMain() async {
|
||||
_Service._shouldRun = true;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await _Service()();
|
||||
}
|
||||
|
||||
class _Service {
|
||||
Future<void> call() async {
|
||||
await app_init.initAppLaunch();
|
||||
await _L10n().init();
|
||||
|
||||
_log.info("[call] Service started");
|
||||
final service = FlutterBackgroundService();
|
||||
service.setForegroundMode(true);
|
||||
final onCancelSubscription = service.onCancel.listen((_) {
|
||||
_log.info("[call] User canceled");
|
||||
_stopSelf();
|
||||
});
|
||||
final onDataSubscription =
|
||||
service.onDataReceived.listen((event) => _onReceiveData(event ?? {}));
|
||||
|
||||
try {
|
||||
await _doWork();
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[call] Uncaught exception", e, stackTrace);
|
||||
}
|
||||
onCancelSubscription.cancel();
|
||||
onDataSubscription.cancel();
|
||||
service.stopBackgroundService();
|
||||
_log.info("[call] Service stopped");
|
||||
}
|
||||
|
||||
Future<void> _doWork() async {
|
||||
final account = Pref().getCurrentAccount();
|
||||
if (account == null) {
|
||||
_log.shout("[_doWork] account == null");
|
||||
return;
|
||||
}
|
||||
final accountPref = AccountPref.of(account);
|
||||
|
||||
final service = FlutterBackgroundService();
|
||||
final metadataTask = _MetadataTask(service, account, accountPref);
|
||||
_metadataTaskStateChangedListener.begin();
|
||||
try {
|
||||
await metadataTask();
|
||||
} finally {
|
||||
_metadataTaskStateChangedListener.end();
|
||||
}
|
||||
}
|
||||
|
||||
void _onReceiveData(Map<String, dynamic> data) {
|
||||
try {
|
||||
for (final e in data.entries) {
|
||||
switch (e.key) {
|
||||
case "stop":
|
||||
_stopSelf();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_onReceiveData] Uncaught exception", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
|
||||
if (ev.state == _metadataTaskState) {
|
||||
return;
|
||||
}
|
||||
_metadataTaskState = ev.state;
|
||||
if (ev.state == MetadataTaskState.waitingForWifi) {
|
||||
final service = FlutterBackgroundService();
|
||||
service.setNotificationInfo(
|
||||
title: _L10n.global().metadataTaskPauseNoWiFiNotification,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _stopSelf() {
|
||||
_log.info("[_stopSelf] Stopping service");
|
||||
FlutterBackgroundService().setNotificationInfo(
|
||||
title: _L10n.global().backgroundServiceStopping,
|
||||
);
|
||||
_shouldRun = false;
|
||||
}
|
||||
|
||||
var _metadataTaskState = MetadataTaskState.idle;
|
||||
late final _metadataTaskStateChangedListener =
|
||||
AppEventListener<MetadataTaskStateChangedEvent>(
|
||||
_onMetadataTaskStateChanged);
|
||||
|
||||
static var _shouldRun = true;
|
||||
static final _log = Logger("service._Service");
|
||||
}
|
||||
|
||||
/// Access localized string out of the main isolate
|
||||
class _L10n {
|
||||
factory _L10n() => _inst;
|
||||
|
||||
_L10n._();
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
final locale = language_util.getSelectedLocale();
|
||||
if (locale == null) {
|
||||
_l10n = await _queryL10n();
|
||||
} else {
|
||||
_l10n = lookupAppLocalizations(locale);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[init] Uncaught exception", e, stackTrace);
|
||||
_l10n = AppLocalizationsEn();
|
||||
}
|
||||
}
|
||||
|
||||
static AppLocalizations global() => _L10n()._l10n;
|
||||
|
||||
Future<AppLocalizations> _queryL10n() async {
|
||||
try {
|
||||
final locale = await Devicelocale.currentAsLocale;
|
||||
return lookupAppLocalizations(locale!);
|
||||
} on FlutterError catch (_) {
|
||||
// unsupported locale, use default (en)
|
||||
return AppLocalizationsEn();
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_queryL10n] Failed while lookupAppLocalizations", e, stackTrace);
|
||||
return AppLocalizationsEn();
|
||||
}
|
||||
}
|
||||
|
||||
static late final _inst = _L10n._();
|
||||
late AppLocalizations _l10n;
|
||||
|
||||
static final _log = Logger("service._L10n");
|
||||
}
|
||||
|
||||
class _MetadataTask {
|
||||
_MetadataTask(this.service, this.account, this.accountPref);
|
||||
|
||||
Future<void> call() async {
|
||||
try {
|
||||
await _updateMetadata();
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[call] Uncaught exception", e, stackTrace);
|
||||
}
|
||||
if (_processedIds.isNotEmpty) {
|
||||
NativeEvent.fire(FileExifUpdatedEvent(_processedIds).toEvent());
|
||||
_processedIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateMetadata() async {
|
||||
final shareFolder = File(
|
||||
path: file_util.unstripPath(account, accountPref.getShareFolderOr()));
|
||||
bool hasScanShareFolder = false;
|
||||
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
||||
for (final r in account.roots) {
|
||||
final dir = File(path: file_util.unstripPath(account, r));
|
||||
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
||||
final updater = UpdateMissingMetadata(fileRepo);
|
||||
await for (final ev in updater(account, dir)) {
|
||||
if (!_Service._shouldRun) {
|
||||
_log.info("[_updateMetadata] Stopping task: user canceled");
|
||||
updater.stop();
|
||||
return;
|
||||
}
|
||||
if (ev is File) {
|
||||
_onFileProcessed(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasScanShareFolder) {
|
||||
final shareUpdater = UpdateMissingMetadata(fileRepo);
|
||||
await for (final ev in shareUpdater(
|
||||
account,
|
||||
shareFolder,
|
||||
isRecursive: false,
|
||||
filter: (f) => f.ownerId != account.username,
|
||||
)) {
|
||||
if (!_Service._shouldRun) {
|
||||
_log.info("[_updateMetadata] Stopping task: user canceled");
|
||||
shareUpdater.stop();
|
||||
return;
|
||||
}
|
||||
if (ev is File) {
|
||||
_onFileProcessed(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onFileProcessed(File file) {
|
||||
++_count;
|
||||
service.setNotificationInfo(
|
||||
title: _L10n.global().metadataTaskProcessingNotification,
|
||||
content: file.strippedPath,
|
||||
);
|
||||
|
||||
_processedIds.add(file.fileId!);
|
||||
if (_processedIds.length >= 10) {
|
||||
NativeEvent.fire(FileExifUpdatedEvent(_processedIds).toEvent());
|
||||
_processedIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
final FlutterBackgroundService service;
|
||||
final Account account;
|
||||
final AccountPref accountPref;
|
||||
|
||||
var _count = 0;
|
||||
var _processedIds = <int>[];
|
||||
|
||||
static final _log = Logger("service._MetadataTask");
|
||||
}
|
||||
|
||||
final _log = Logger("service");
|
|
@ -86,6 +86,9 @@ class UpdateMissingMetadata {
|
|||
metadata: OrNull(metadataObj),
|
||||
);
|
||||
yield file;
|
||||
|
||||
// slow down a bit to give some space for the main isolate
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[call] Failed while updating metadata: ${file.path}", e,
|
||||
stackTrace);
|
||||
|
|
|
@ -22,8 +22,10 @@ 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/metadata_task_manager.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/primitive.dart';
|
||||
import 'package:nc_photos/service.dart' as service;
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
|
@ -66,17 +68,14 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
super.initState();
|
||||
_thumbZoomLevel = Pref().getHomePhotosZoomLevelOr(0);
|
||||
_initBloc();
|
||||
_metadataTaskStateChangedListener.begin();
|
||||
_web?.onInitState();
|
||||
_prefUpdatedListener.begin();
|
||||
_filePropertyUpdatedListener.begin();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_metadataTaskIconController.stop();
|
||||
_metadataTaskStateChangedListener.end();
|
||||
_prefUpdatedListener.end();
|
||||
_filePropertyUpdatedListener.end();
|
||||
_web?.onDispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -137,8 +136,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
controller: _scrollController,
|
||||
slivers: [
|
||||
_buildAppBar(context),
|
||||
if (_metadataTaskState != MetadataTaskState.idle)
|
||||
_buildMetadataTaskHeader(context),
|
||||
_web?.buildContent(context),
|
||||
if (AccountPref.of(widget.account)
|
||||
.isEnableMemoryAlbumOr(true) &&
|
||||
_smartAlbums.isNotEmpty)
|
||||
|
@ -151,7 +149,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
].whereType<Widget>().toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -260,81 +258,6 @@ 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.global().metadataTaskProcessingNotification +
|
||||
_getMetadataTaskProgressString(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (_metadataTaskState == MetadataTaskState.waitingForWifi)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.sync_problem,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
L10n.global().metadataTaskPauseNoWiFiNotification,
|
||||
style: const 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.global().configButtonLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(Settings.routeName,
|
||||
arguments: SettingsArguments(widget.account));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmartAlbumList(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
|
@ -487,48 +410,44 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
);
|
||||
}
|
||||
|
||||
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
|
||||
if (ev.state == MetadataTaskState.idle) {
|
||||
_metadataTaskProcessCount = 0;
|
||||
}
|
||||
if (ev.state != _metadataTaskState) {
|
||||
setState(() {
|
||||
_metadataTaskState = ev.state;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onPrefUpdated(PrefUpdatedEvent ev) {
|
||||
if (ev.key == PrefKey.enableExif && ev.value == true) {
|
||||
_tryStartMetadataTask(ignoreFired: true);
|
||||
if (ev.key == PrefKey.enableExif) {
|
||||
if (ev.value == true) {
|
||||
_tryStartMetadataTask(ignoreFired: true);
|
||||
} else {
|
||||
_stopMetadataTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
|
||||
if (!ev.hasAnyProperties([FilePropertyUpdatedEvent.propMetadata])) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
++_metadataTaskProcessCount;
|
||||
});
|
||||
}
|
||||
|
||||
void _tryStartMetadataTask({
|
||||
bool ignoreFired = false,
|
||||
}) {
|
||||
if (_bloc.state is ScanAccountDirBlocSuccess &&
|
||||
Pref().isEnableExifOr() &&
|
||||
(!_hasFiredMetadataTask.value || ignoreFired)) {
|
||||
MetadataTaskManager().addTask(
|
||||
MetadataTask(widget.account, AccountPref.of(widget.account)));
|
||||
_metadataTaskProcessTotalCount = _backingFiles
|
||||
final missingMetadataCount = _backingFiles
|
||||
.where(
|
||||
(f) => file_util.isSupportedImageFormat(f) && f.metadata == null)
|
||||
.length;
|
||||
if (missingMetadataCount > 0) {
|
||||
if (_web != null) {
|
||||
_web!.startMetadataTask(missingMetadataCount);
|
||||
} else {
|
||||
service.startService();
|
||||
}
|
||||
}
|
||||
|
||||
_hasFiredMetadataTask.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _stopMetadataTask() {
|
||||
if (_web == null) {
|
||||
service.stopService();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncFavorite() async {
|
||||
if (!_hasResyncedFavorites.value) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
|
@ -622,10 +541,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
if (_itemListMaxExtent != null &&
|
||||
constraints.hasBoundedHeight &&
|
||||
_appBarExtent != null) {
|
||||
final metadataTaskHeaderExtent =
|
||||
_metadataTaskState == MetadataTaskState.idle
|
||||
? 0
|
||||
: _metadataTaskHeaderHeight;
|
||||
final metadataTaskHeaderExtent = _web?.getHeaderHeight() ?? 0;
|
||||
final smartAlbumListHeight =
|
||||
AccountPref.of(widget.account).isEnableMemoryAlbumOr(true) &&
|
||||
_smartAlbums.isNotEmpty
|
||||
|
@ -647,15 +563,6 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
}
|
||||
|
||||
String _getMetadataTaskProgressString() {
|
||||
if (_metadataTaskProcessTotalCount == 0) {
|
||||
return "";
|
||||
}
|
||||
final clippedCount =
|
||||
math.min(_metadataTaskProcessCount, _metadataTaskProcessTotalCount - 1);
|
||||
return " ($clippedCount/$_metadataTaskProcessTotalCount)";
|
||||
}
|
||||
|
||||
Primitive<bool> get _hasFiredMetadataTask {
|
||||
final name = bloc_util.getInstNameForRootAwareAccount(
|
||||
"HomePhotosState.hasFiredMetadataTask", widget.account);
|
||||
|
@ -699,12 +606,161 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
double? _appBarExtent;
|
||||
double? _itemListMaxExtent;
|
||||
|
||||
late final _prefUpdatedListener =
|
||||
AppEventListener<PrefUpdatedEvent>(_onPrefUpdated);
|
||||
|
||||
late final _Web? _web = platform_k.isWeb ? _Web(this) : null;
|
||||
|
||||
static final _log = Logger("widget.home_photos._HomePhotosState");
|
||||
static const _menuValueRefresh = 0;
|
||||
}
|
||||
|
||||
class _Web {
|
||||
_Web(this.state);
|
||||
|
||||
void onInitState() {
|
||||
_metadataTaskStateChangedListener.begin();
|
||||
_filePropertyUpdatedListener.begin();
|
||||
}
|
||||
|
||||
void onDispose() {
|
||||
_metadataTaskIconController.stop();
|
||||
_metadataTaskStateChangedListener.end();
|
||||
_filePropertyUpdatedListener.end();
|
||||
}
|
||||
|
||||
Widget? buildContent(BuildContext context) {
|
||||
if (_metadataTaskState != MetadataTaskState.idle) {
|
||||
return _buildMetadataTaskHeader(context);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void startMetadataTask(int missingMetadataCount) {
|
||||
MetadataTaskManager().addTask(MetadataTask(
|
||||
state.widget.account, AccountPref.of(state.widget.account)));
|
||||
_metadataTaskProcessTotalCount = missingMetadataCount;
|
||||
}
|
||||
|
||||
double getHeaderHeight() {
|
||||
return _metadataTaskState == MetadataTaskState.idle
|
||||
? 0
|
||||
: _metadataTaskHeaderHeight;
|
||||
}
|
||||
|
||||
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.global().metadataTaskProcessingNotification +
|
||||
_getMetadataTaskProgressString(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (_metadataTaskState == MetadataTaskState.waitingForWifi)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.sync_problem,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
L10n.global().metadataTaskPauseNoWiFiNotification,
|
||||
style: const 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.global().configButtonLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(Settings.routeName,
|
||||
arguments: SettingsArguments(state.widget.account));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
|
||||
if (ev.state == MetadataTaskState.idle) {
|
||||
_metadataTaskProcessCount = 0;
|
||||
}
|
||||
if (ev.state != _metadataTaskState) {
|
||||
// ignore: invalid_use_of_protected_member
|
||||
state.setState(() {
|
||||
_metadataTaskState = ev.state;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
|
||||
if (!ev.hasAnyProperties([FilePropertyUpdatedEvent.propMetadata])) {
|
||||
return;
|
||||
}
|
||||
// ignore: invalid_use_of_protected_member
|
||||
state.setState(() {
|
||||
++_metadataTaskProcessCount;
|
||||
});
|
||||
}
|
||||
|
||||
String _getMetadataTaskProgressString() {
|
||||
if (_metadataTaskProcessTotalCount == 0) {
|
||||
return "";
|
||||
}
|
||||
final clippedCount =
|
||||
math.min(_metadataTaskProcessCount, _metadataTaskProcessTotalCount - 1);
|
||||
return " ($clippedCount/$_metadataTaskProcessTotalCount)";
|
||||
}
|
||||
|
||||
final _HomePhotosState state;
|
||||
|
||||
late final _metadataTaskStateChangedListener =
|
||||
AppEventListener<MetadataTaskStateChangedEvent>(
|
||||
_onMetadataTaskStateChanged);
|
||||
var _metadataTaskState = MetadataTaskManager().state;
|
||||
late final _prefUpdatedListener =
|
||||
AppEventListener<PrefUpdatedEvent>(_onPrefUpdated);
|
||||
late final _filePropertyUpdatedListener =
|
||||
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdated);
|
||||
var _metadataTaskProcessCount = 0;
|
||||
|
@ -712,12 +768,9 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
late final _metadataTaskIconController = AnimationController(
|
||||
upperBound: 2 * math.pi,
|
||||
duration: const Duration(seconds: 10),
|
||||
vsync: this,
|
||||
vsync: state,
|
||||
)..repeat();
|
||||
|
||||
static final _log = Logger("widget.home_photos._HomePhotosState");
|
||||
static const _menuValueRefresh = 0;
|
||||
|
||||
static const _metadataTaskHeaderHeight = 32.0;
|
||||
}
|
||||
|
||||
|
|
|
@ -232,6 +232,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
devicelocale:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: devicelocale
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -297,6 +304,15 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_background_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v0.2.6-nc-photos-1"
|
||||
resolved-ref: c63c7ed595d39897db4791266e6208abd7d688e5
|
||||
url: "https://gitlab.com/nc-photos/flutter_background_service.git"
|
||||
source: git
|
||||
version: "0.2.6"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -32,6 +32,7 @@ dependencies:
|
|||
cached_network_image: ^3.0.0
|
||||
collection: ^1.15.0
|
||||
connectivity_plus: ^2.0.2
|
||||
devicelocale: ^0.5.0
|
||||
device_info_plus: ^3.1.0
|
||||
draggable_scrollbar:
|
||||
git:
|
||||
|
@ -43,6 +44,10 @@ dependencies:
|
|||
git:
|
||||
url: https://gitlab.com/nc-photos/exifdart.git
|
||||
ref: 1.1.0
|
||||
flutter_background_service:
|
||||
git:
|
||||
url: https://gitlab.com/nc-photos/flutter_background_service.git
|
||||
ref: v0.2.6-nc-photos-1
|
||||
flutter_bloc: ^7.0.0
|
||||
flutter_map: ^0.14.0
|
||||
flutter_staggered_grid_view:
|
||||
|
|
Loading…
Reference in a new issue