From a0b62cac3582b3a73933337a9cbb9c08c3ac668a Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 11 Sep 2021 01:10:26 +0800 Subject: [PATCH] Migrate to new person API --- lib/api/api.dart | 38 ++++++++++++ lib/api/api_util.dart | 9 ++- lib/bloc/list_face.dart | 99 ++++++++++++++++++++++++++++++ lib/entity/face.dart | 42 +++++++++++++ lib/entity/face/data_source.dart | 59 ++++++++++++++++++ lib/entity/person.dart | 41 +++++-------- lib/entity/person/data_source.dart | 11 +--- lib/use_case/populate_person.dart | 8 +-- lib/widget/people_browser.dart | 7 +-- lib/widget/person_browser.dart | 91 ++++++++++++++++++--------- 10 files changed, 329 insertions(+), 76 deletions(-) create mode 100644 lib/bloc/list_face.dart create mode 100644 lib/entity/face.dart create mode 100644 lib/entity/face/data_source.dart diff --git a/lib/api/api.dart b/lib/api/api.dart index 003cad44..ac329a2d 100644 --- a/lib/api/api.dart +++ b/lib/api/api.dart @@ -453,6 +453,8 @@ class _OcsFacerecognition { _OcsFacerecognition(this._ocs); _OcsFacerecognitionPersons persons() => _OcsFacerecognitionPersons(this); + _OcsFacerecognitionPerson person(String name) => + _OcsFacerecognitionPerson(this, name); final _Ocs _ocs; } @@ -483,6 +485,42 @@ class _OcsFacerecognitionPersons { static final _log = Logger("api.api._OcsFacerecognitionPersons"); } +class _OcsFacerecognitionPerson { + _OcsFacerecognitionPerson(this._facerecognition, this._name); + + _OcsFacerecognitionPersonFaces faces() => + _OcsFacerecognitionPersonFaces(this); + + final _OcsFacerecognition _facerecognition; + final String _name; +} + +class _OcsFacerecognitionPersonFaces { + _OcsFacerecognitionPersonFaces(this._person); + + Future get() async { + try { + return await _person._facerecognition._ocs._api.request( + "GET", + "ocs/v2.php/apps/facerecognition/api/v1/person/${_person._name}/faces", + header: { + "OCS-APIRequest": "true", + }, + queryParameters: { + "format": "json", + }, + ); + } catch (e) { + _log.severe("[get] Failed while get", e); + rethrow; + } + } + + final _OcsFacerecognitionPerson _person; + + static final _log = Logger("api.api._OcsFacerecognitionPersonFaces"); +} + class _OcsFilesSharing { _OcsFilesSharing(this._ocs); diff --git a/lib/api/api_util.dart b/lib/api/api_util.dart index ae4d80a3..8e70662e 100644 --- a/lib/api/api_util.dart +++ b/lib/api/api_util.dart @@ -4,7 +4,6 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; -import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/exception.dart'; /// Return the preview image URL for [file]. See [getFilePreviewUrlRelative] @@ -71,20 +70,20 @@ String getTrashbinPath(Account account) => /// Return the face image URL. See [getFacePreviewUrlRelative] String getFacePreviewUrl( Account account, - Face face, { + int faceId, { required int size, }) { return "${account.url}/" - "${getFacePreviewUrlRelative(account, face, size: size)}"; + "${getFacePreviewUrlRelative(account, faceId, size: size)}"; } /// Return the relative URL of the face image String getFacePreviewUrlRelative( Account account, - Face face, { + int faceId, { required int size, }) { - return "index.php/apps/facerecognition/face/${face.id}/thumb/$size"; + return "index.php/apps/facerecognition/face/$faceId/thumb/$size"; } /// Query the app password for [account] diff --git a/lib/bloc/list_face.dart b/lib/bloc/list_face.dart new file mode 100644 index 00000000..9de6d32f --- /dev/null +++ b/lib/bloc/list_face.dart @@ -0,0 +1,99 @@ +import 'package:bloc/bloc.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/entity/face.dart'; +import 'package:nc_photos/entity/face/data_source.dart'; +import 'package:nc_photos/entity/person.dart'; + +abstract class ListFaceBlocEvent { + const ListFaceBlocEvent(); +} + +class ListFaceBlocQuery extends ListFaceBlocEvent { + const ListFaceBlocQuery(this.account, this.person); + + @override + toString() { + return "$runtimeType {" + "account: $account, " + "person: $person, " + "}"; + } + + final Account account; + final Person person; +} + +abstract class ListFaceBlocState { + const ListFaceBlocState(this.account, this.items); + + @override + toString() { + return "$runtimeType {" + "account: $account, " + "items: List {length: ${items.length}}, " + "}"; + } + + final Account? account; + final List items; +} + +class ListFaceBlocInit extends ListFaceBlocState { + ListFaceBlocInit() : super(null, const []); +} + +class ListFaceBlocLoading extends ListFaceBlocState { + const ListFaceBlocLoading(Account? account, List items) + : super(account, items); +} + +class ListFaceBlocSuccess extends ListFaceBlocState { + const ListFaceBlocSuccess(Account? account, List items) + : super(account, items); +} + +class ListFaceBlocFailure extends ListFaceBlocState { + const ListFaceBlocFailure(Account? account, List items, this.exception) + : super(account, items); + + @override + toString() { + return "$runtimeType {" + "super: ${super.toString()}, " + "exception: $exception, " + "}"; + } + + final dynamic exception; +} + +/// List all people recognized in an account +class ListFaceBloc extends Bloc { + ListFaceBloc() : super(ListFaceBlocInit()); + + @override + mapEventToState(ListFaceBlocEvent event) async* { + _log.info("[mapEventToState] $event"); + if (event is ListFaceBlocQuery) { + yield* _onEventQuery(event); + } + } + + Stream _onEventQuery(ListFaceBlocQuery ev) async* { + try { + yield ListFaceBlocLoading(ev.account, state.items); + yield ListFaceBlocSuccess(ev.account, await _query(ev)); + } catch (e, stackTrace) { + _log.severe("[_onEventQuery] Exception while request", e, stackTrace); + yield ListFaceBlocFailure(ev.account, state.items, e); + } + } + + Future> _query(ListFaceBlocQuery ev) { + final personRepo = FaceRepo(FaceRemoteDataSource()); + return personRepo.list(ev.account, ev.person); + } + + static final _log = Logger("bloc.list_personListFaceBloc"); +} diff --git a/lib/entity/face.dart b/lib/entity/face.dart new file mode 100644 index 00000000..116b4f95 --- /dev/null +++ b/lib/entity/face.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/entity/person.dart'; + +class Face with EquatableMixin { + Face({ + required this.id, + required this.fileId, + }); + + @override + toString() { + return "$runtimeType {" + "id: '$id', " + "fileId: '$fileId', " + "}"; + } + + @override + get props => [ + id, + fileId, + ]; + + final int id; + final int fileId; +} + +class FaceRepo { + const FaceRepo(this.dataSrc); + + /// See [FaceDataSource.list] + Future> list(Account account, Person person) => + this.dataSrc.list(account, person); + + final FaceDataSource dataSrc; +} + +abstract class FaceDataSource { + /// List all faces associated to [person] + Future> list(Account account, Person person); +} diff --git a/lib/entity/face/data_source.dart b/lib/entity/face/data_source.dart new file mode 100644 index 00000000..9324f1d8 --- /dev/null +++ b/lib/entity/face/data_source.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/api/api.dart'; +import 'package:nc_photos/entity/face.dart'; +import 'package:nc_photos/entity/person.dart'; +import 'package:nc_photos/exception.dart'; +import 'package:nc_photos/type.dart'; + +class FaceRemoteDataSource implements FaceDataSource { + const FaceRemoteDataSource(); + + @override + list(Account account, Person person) async { + _log.info("[list] $person"); + final response = await Api(account) + .ocs() + .facerecognition() + .person(person.name) + .faces() + .get(); + if (!response.isGood) { + _log.severe("[list] Failed requesting server: $response"); + throw ApiException( + response: response, + message: "Failed communicating with server: ${response.statusCode}"); + } + + final json = jsonDecode(response.body); + final List dataJson = json["ocs"]["data"].cast(); + return _FaceParser().parseList(dataJson); + } + + static final _log = Logger("entity.face.data_source.FaceRemoteDataSource"); +} + +class _FaceParser { + List parseList(List jsons) { + final product = []; + for (final j in jsons) { + try { + product.add(parseSingle(j)); + } catch (e) { + _log.severe("[parseList] Failed parsing json: ${jsonEncode(j)}", e); + } + } + return product; + } + + Face parseSingle(JsonObj json) { + return Face( + id: json["id"], + fileId: json["fileId"], + ); + } + + static final _log = Logger("entity.face.data_source._FaceParser"); +} diff --git a/lib/entity/person.dart b/lib/entity/person.dart index d33e118c..0f9a83bc 100644 --- a/lib/entity/person.dart +++ b/lib/entity/person.dart @@ -1,39 +1,32 @@ import 'package:equatable/equatable.dart'; import 'package:nc_photos/account.dart'; -class Face with EquatableMixin { - Face({ - required this.id, - required this.fileId, +class Person with EquatableMixin { + Person({ + required this.name, + required this.thumbFaceId, + required this.count, }); @override - get props => [ - id, - fileId, - ]; - - final int id; - final int fileId; -} - -class Person with EquatableMixin { - Person({ - this.name, - required this.id, - required this.faces, - }); + toString() { + return "$runtimeType {" + "name: '$name', " + "thumbFaceId: '$thumbFaceId', " + "count: '$count', " + "}"; + } @override get props => [ name, - id, - faces, + thumbFaceId, + count, ]; - final String? name; - final int id; - final List faces; + final String name; + final int thumbFaceId; + final int count; } class PersonRepo { diff --git a/lib/entity/person/data_source.dart b/lib/entity/person/data_source.dart index dbd6c36e..df225f2d 100644 --- a/lib/entity/person/data_source.dart +++ b/lib/entity/person/data_source.dart @@ -44,17 +44,10 @@ class _PersonParser { } Person parseSingle(JsonObj json) { - final faces = (json["faces"] as List) - .cast() - .map((e) => Face( - id: e["id"], - fileId: e["file-id"], - )) - .toList(); return Person( name: json["name"], - id: json["id"], - faces: faces, + thumbFaceId: json["thumbFaceId"], + count: json["count"], ); } diff --git a/lib/use_case/populate_person.dart b/lib/use_case/populate_person.dart index c2ee3d3e..85b68c54 100644 --- a/lib/use_case/populate_person.dart +++ b/lib/use_case/populate_person.dart @@ -2,21 +2,21 @@ import 'package:idb_shim/idb_client.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_db.dart'; +import 'package:nc_photos/entity/face.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; -import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/exception.dart'; class PopulatePerson { - /// Return a list of files belonging to this person - Future> call(Account account, Person person) async { + /// Return a list of files of the faces + Future> call(Account account, List faces) async { return await AppDb.use((db) async { final transaction = db.transaction(AppDb.fileDbStoreName, idbModeReadOnly); final store = transaction.objectStore(AppDb.fileDbStoreName); final index = store.index(AppDbFileDbEntry.indexName); final products = []; - for (final f in person.faces) { + for (final f in faces) { try { products.add(await _populateOne(account, f, store, index)); } catch (e, stackTrace) { diff --git a/lib/widget/people_browser.dart b/lib/widget/people_browser.dart index 1d2b9985..0aad72c0 100644 --- a/lib/widget/people_browser.dart +++ b/lib/widget/people_browser.dart @@ -191,12 +191,11 @@ class _PeopleBrowserState extends State { void _transformItems(List items) { _items = items - .where((element) => element.name != null) - .sorted((a, b) => a.name!.compareTo(b.name!)) + .sorted((a, b) => a.name.compareTo(b.name)) .map((e) => _PersonListItem( account: widget.account, - name: e.name!, - faceUrl: api_util.getFacePreviewUrl(widget.account, e.faces.first, + name: e.name, + faceUrl: api_util.getFacePreviewUrl(widget.account, e.thumbFaceId, size: 256), onTap: () => _onItemTap(e), )) diff --git a/lib/widget/person_browser.dart b/lib/widget/person_browser.dart index b9b79951..50c4949d 100644 --- a/lib/widget/person_browser.dart +++ b/lib/widget/person_browser.dart @@ -1,26 +1,32 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.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/account.dart'; import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc/list_face.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/provider.dart'; +import 'package:nc_photos/entity/face.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/entity/person.dart'; 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; import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/share_handler.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/use_case/add_to_album.dart'; @@ -74,7 +80,7 @@ class _PersonBrowserState extends State @override initState() { super.initState(); - _initPerson(); + _initBloc(); _thumbZoomLevel = Pref.inst().getAlbumBrowserZoomLevelOr(0); _filePropertyUpdatedListener.begin(); @@ -90,20 +96,21 @@ class _PersonBrowserState extends State build(BuildContext context) { return AppTheme( child: Scaffold( - body: Builder( - builder: (context) => _buildContent(context), + body: BlocListener( + bloc: _bloc, + listener: (context, state) => _onStateChange(context, state), + child: BlocBuilder( + bloc: _bloc, + builder: (context, state) => _buildContent(context), + ), ), ), ); } - void _initPerson() async { - final items = await PopulatePerson()(widget.account, widget.person); - if (mounted) { - setState(() { - _transformItems(items); - }); - } + void _initBloc() { + _log.info("[_initBloc] Initialize bloc"); + _reqQuery(); } Widget _buildContent(BuildContext context) { @@ -162,7 +169,7 @@ class _PersonBrowserState extends State mainAxisSize: MainAxisSize.min, children: [ Text( - widget.person.name!, + widget.person.name, style: TextStyle( color: AppTheme.getPrimaryTextColor(context), ), @@ -172,7 +179,7 @@ class _PersonBrowserState extends State ), if (_backingFiles != null) Text( - "${_backingFiles!.length} photos", + "${itemStreamListItems.length} photos", style: TextStyle( color: AppTheme.getSecondaryTextColor(context), fontSize: 12, @@ -208,7 +215,7 @@ class _PersonBrowserState extends State fit: BoxFit.cover, child: CachedNetworkImage( imageUrl: api_util.getFacePreviewUrl( - widget.account, widget.person.faces.first, + widget.account, widget.person.thumbFaceId, size: 64), httpHeaders: { "Authorization": Api.getAuthorizationHeaderValue(widget.account), @@ -284,6 +291,20 @@ class _PersonBrowserState extends State ); } + void _onStateChange(BuildContext context, ListFaceBlocState state) { + if (state is ListFaceBlocInit) { + _backingFiles = null; + } else if (state is ListFaceBlocSuccess || state is ListFaceBlocLoading) { + _transformItems(state.items); + } else if (state is ListFaceBlocFailure) { + _transformItems(state.items); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(state.exception)), + duration: k.snackBarDurationNormal, + )); + } + } + void _onItemTap(int index) { Navigator.pushNamed(context, Viewer.routeName, arguments: ViewerArguments(widget.account, _backingFiles!, index)); @@ -420,26 +441,33 @@ class _PersonBrowserState extends State ); } - void _transformItems(List items) { - _backingFiles = items + void _transformItems(List items) async { + final files = await PopulatePerson()(widget.account, items); + _backingFiles = files .sorted(compareFileDateTimeDescending) .where((element) => file_util.isSupportedFormat(element) && element.isArchived != true) .toList(); - itemStreamListItems = _backingFiles! - .mapWithIndex((i, f) => _ListItem( - index: i, - file: f, - account: widget.account, - previewUrl: api_util.getFilePreviewUrl( - widget.account, - f, - width: _thumbSize, - height: _thumbSize, - ), - onTap: () => _onItemTap(i), - )) - .toList(); + setState(() { + itemStreamListItems = _backingFiles! + .mapWithIndex((i, f) => _ListItem( + index: i, + file: f, + account: widget.account, + previewUrl: api_util.getFilePreviewUrl( + widget.account, + f, + width: _thumbSize, + height: _thumbSize, + ), + onTap: () => _onItemTap(i), + )) + .toList(); + }); + } + + void _reqQuery() { + _bloc.add(ListFaceBlocQuery(widget.account, widget.person)); } int get _thumbSize { @@ -456,13 +484,16 @@ class _PersonBrowserState extends State } } + final ListFaceBloc _bloc = ListFaceBloc(); List? _backingFiles; var _thumbZoomLevel = 0; late final Throttler _refreshThrottler = Throttler( onTriggered: (_) { - _initPerson(); + if (mounted) { + _transformItems(_bloc.state.items); + } }, logTag: "_PersonBrowserState.refresh", );