Migrate to new person API

This commit is contained in:
Ming Ming 2021-09-11 01:10:26 +08:00
parent 620c840ccc
commit a0b62cac35
10 changed files with 329 additions and 76 deletions

View file

@ -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<Response> 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);

View file

@ -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]

99
lib/bloc/list_face.dart Normal file
View file

@ -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<Face> items;
}
class ListFaceBlocInit extends ListFaceBlocState {
ListFaceBlocInit() : super(null, const []);
}
class ListFaceBlocLoading extends ListFaceBlocState {
const ListFaceBlocLoading(Account? account, List<Face> items)
: super(account, items);
}
class ListFaceBlocSuccess extends ListFaceBlocState {
const ListFaceBlocSuccess(Account? account, List<Face> items)
: super(account, items);
}
class ListFaceBlocFailure extends ListFaceBlocState {
const ListFaceBlocFailure(Account? account, List<Face> 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<ListFaceBlocEvent, ListFaceBlocState> {
ListFaceBloc() : super(ListFaceBlocInit());
@override
mapEventToState(ListFaceBlocEvent event) async* {
_log.info("[mapEventToState] $event");
if (event is ListFaceBlocQuery) {
yield* _onEventQuery(event);
}
}
Stream<ListFaceBlocState> _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<List<Face>> _query(ListFaceBlocQuery ev) {
final personRepo = FaceRepo(FaceRemoteDataSource());
return personRepo.list(ev.account, ev.person);
}
static final _log = Logger("bloc.list_personListFaceBloc");
}

42
lib/entity/face.dart Normal file
View file

@ -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<Face>> list(Account account, Person person) =>
this.dataSrc.list(account, person);
final FaceDataSource dataSrc;
}
abstract class FaceDataSource {
/// List all faces associated to [person]
Future<List<Face>> list(Account account, Person person);
}

View file

@ -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<JsonObj> dataJson = json["ocs"]["data"].cast<JsonObj>();
return _FaceParser().parseList(dataJson);
}
static final _log = Logger("entity.face.data_source.FaceRemoteDataSource");
}
class _FaceParser {
List<Face> parseList(List<JsonObj> jsons) {
final product = <Face>[];
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");
}

View file

@ -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<Face> faces;
final String name;
final int thumbFaceId;
final int count;
}
class PersonRepo {

View file

@ -44,17 +44,10 @@ class _PersonParser {
}
Person parseSingle(JsonObj json) {
final faces = (json["faces"] as List)
.cast<JsonObj>()
.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"],
);
}

View file

@ -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<List<File>> call(Account account, Person person) async {
/// Return a list of files of the faces
Future<List<File>> call(Account account, List<Face> 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 = <File>[];
for (final f in person.faces) {
for (final f in faces) {
try {
products.add(await _populateOne(account, f, store, index));
} catch (e, stackTrace) {

View file

@ -191,12 +191,11 @@ class _PeopleBrowserState extends State<PeopleBrowser> {
void _transformItems(List<Person> 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),
))

View file

@ -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<PersonBrowser>
@override
initState() {
super.initState();
_initPerson();
_initBloc();
_thumbZoomLevel = Pref.inst().getAlbumBrowserZoomLevelOr(0);
_filePropertyUpdatedListener.begin();
@ -90,20 +96,21 @@ class _PersonBrowserState extends State<PersonBrowser>
build(BuildContext context) {
return AppTheme(
child: Scaffold(
body: Builder(
builder: (context) => _buildContent(context),
body: BlocListener<ListFaceBloc, ListFaceBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ListFaceBloc, ListFaceBlocState>(
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<PersonBrowser>
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<PersonBrowser>
),
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<PersonBrowser>
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<PersonBrowser>
);
}
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<PersonBrowser>
);
}
void _transformItems(List<File> items) {
_backingFiles = items
void _transformItems(List<Face> 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<PersonBrowser>
}
}
final ListFaceBloc _bloc = ListFaceBloc();
List<File>? _backingFiles;
var _thumbZoomLevel = 0;
late final Throttler _refreshThrottler = Throttler(
onTriggered: (_) {
_initPerson();
if (mounted) {
_transformItems(_bloc.state.items);
}
},
logTag: "_PersonBrowserState.refresh",
);