mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Migrate to new person API
This commit is contained in:
parent
620c840ccc
commit
a0b62cac35
10 changed files with 329 additions and 76 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
99
lib/bloc/list_face.dart
Normal 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
42
lib/entity/face.dart
Normal 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);
|
||||
}
|
59
lib/entity/face/data_source.dart
Normal file
59
lib/entity/face/data_source.dart
Normal 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");
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
))
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue