mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Add a button to list all people in a dedicated view
This commit is contained in:
parent
bc16dc1530
commit
7fe757ad5e
6 changed files with 352 additions and 20 deletions
|
@ -92,7 +92,7 @@ class ListPersonBloc extends Bloc<ListPersonBlocEvent, ListPersonBlocState> {
|
|||
}
|
||||
|
||||
Future<List<Person>> _query(ListPersonBlocQuery ev) =>
|
||||
ListPerson(_c)(ev.account);
|
||||
ListPerson(_c.withLocalRepo())(ev.account);
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
|
|
|
@ -1370,6 +1370,10 @@
|
|||
"@searchFilterBubbleFavoriteFalseText": {
|
||||
"description": "List of active search filters shown in the result page (by favorites, false)"
|
||||
},
|
||||
"showAllButtonLabel": "SHOW ALL",
|
||||
"@showAllButtonLabel": {
|
||||
"description": "A button to show all items of a certain item group (e.g., show all recognized faces)"
|
||||
},
|
||||
|
||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||
"@errorUnauthenticated": {
|
||||
|
|
|
@ -142,6 +142,7 @@
|
|||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -302,6 +303,7 @@
|
|||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -342,12 +344,14 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"settingsLanguageOptionSystemDefaultLabel",
|
||||
"rootPickerSkipConfirmationDialogContent2"
|
||||
"rootPickerSkipConfirmationDialogContent2",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
|
@ -368,7 +372,8 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
|
@ -433,7 +438,8 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
|
@ -515,7 +521,8 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
@ -576,7 +583,8 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -637,7 +645,8 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -698,7 +707,8 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
],
|
||||
|
||||
"zh_Hant": [
|
||||
|
@ -759,6 +769,7 @@
|
|||
"searchFilterBubbleTypeVideoText",
|
||||
"searchFilterFavoriteLabel",
|
||||
"searchFilterBubbleFavoriteTrueText",
|
||||
"searchFilterBubbleFavoriteFalseText"
|
||||
"searchFilterBubbleFavoriteFalseText",
|
||||
"showAllButtonLabel"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import 'package:nc_photos/widget/enhanced_photo_browser.dart';
|
|||
import 'package:nc_photos/widget/home.dart';
|
||||
import 'package:nc_photos/widget/image_editor.dart';
|
||||
import 'package:nc_photos/widget/local_file_viewer.dart';
|
||||
import 'package:nc_photos/widget/people_browser.dart';
|
||||
import 'package:nc_photos/widget/person_browser.dart';
|
||||
import 'package:nc_photos/widget/root_picker.dart';
|
||||
import 'package:nc_photos/widget/settings.dart';
|
||||
|
@ -165,6 +166,7 @@ class _MyAppState extends State<MyApp>
|
|||
route ??= _handleImageEditorRoute(settings);
|
||||
route ??= _handleChangelogRoute(settings);
|
||||
route ??= _handleTagBrowserRoute(settings);
|
||||
route ??= _handlePeopleBrowserRoute(settings);
|
||||
return route;
|
||||
}
|
||||
|
||||
|
@ -555,6 +557,19 @@ class _MyAppState extends State<MyApp>
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic>? _handlePeopleBrowserRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == PeopleBrowser.routeName &&
|
||||
settings.arguments != null) {
|
||||
final args = settings.arguments as PeopleBrowserArguments;
|
||||
return PeopleBrowser.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handlePeopleBrowserRoute] Failed while handling route", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
|
292
app/lib/widget/people_browser.dart
Normal file
292
app/lib/widget/people_browser.dart
Normal file
|
@ -0,0 +1,292 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:kiwi/kiwi.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_person.dart';
|
||||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/widget/person_browser.dart';
|
||||
|
||||
class PeopleBrowserArguments {
|
||||
const PeopleBrowserArguments(this.account);
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
/// Show a list of all people associated with this account
|
||||
class PeopleBrowser extends StatefulWidget {
|
||||
static const routeName = "/people-browser";
|
||||
|
||||
static Route buildRoute(PeopleBrowserArguments args) => MaterialPageRoute(
|
||||
builder: (context) => PeopleBrowser.fromArgs(args),
|
||||
);
|
||||
|
||||
const PeopleBrowser({
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
PeopleBrowser.fromArgs(PeopleBrowserArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
);
|
||||
|
||||
@override
|
||||
createState() => _PeopleBrowserState();
|
||||
|
||||
final Account account;
|
||||
}
|
||||
|
||||
class _PeopleBrowserState extends State<PeopleBrowser> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_initBloc();
|
||||
}
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return AppTheme(
|
||||
child: Scaffold(
|
||||
body: BlocListener<ListPersonBloc, ListPersonBlocState>(
|
||||
bloc: _bloc,
|
||||
listener: (context, state) => _onStateChange(context, state),
|
||||
child: BlocBuilder<ListPersonBloc, ListPersonBlocState>(
|
||||
bloc: _bloc,
|
||||
builder: (context, state) => _buildContent(context, state),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is ListPersonBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
} else {
|
||||
// process the current state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_onStateChange(context, _bloc.state);
|
||||
});
|
||||
});
|
||||
}
|
||||
_reqQuery();
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, ListPersonBlocState state) {
|
||||
return Stack(
|
||||
children: [
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: Theme.of(context).colorScheme.copyWith(
|
||||
secondary: AppTheme.getOverscrollIndicatorColor(context),
|
||||
),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
_buildAppBar(context),
|
||||
if (state is ListPersonBlocLoading)
|
||||
const SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverStaggeredGrid.extentBuilder(
|
||||
maxCrossAxisExtent: 160,
|
||||
mainAxisSpacing: 8,
|
||||
itemCount: _items.length,
|
||||
itemBuilder: _buildItem,
|
||||
staggeredTileBuilder: (_) => const StaggeredTile.count(1, 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
title: Text(L10n.global().collectionPeopleLabel),
|
||||
floating: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, int index) {
|
||||
final item = _items[index];
|
||||
return item.buildWidget(context);
|
||||
}
|
||||
|
||||
void _onStateChange(BuildContext context, ListPersonBlocState state) {
|
||||
if (state is ListPersonBlocInit) {
|
||||
_items = [];
|
||||
} else if (state is ListPersonBlocSuccess ||
|
||||
state is ListPersonBlocLoading) {
|
||||
_transformItems(state.items);
|
||||
} else if (state is ListPersonBlocFailure) {
|
||||
_transformItems(state.items);
|
||||
try {
|
||||
final e = state.exception as ApiException;
|
||||
if (e.response.statusCode == 404) {
|
||||
// face recognition app probably not installed, ignore
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(exception_util.toUserString(state.exception)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onItemTap(Person person) {
|
||||
Navigator.pushNamed(context, PersonBrowser.routeName,
|
||||
arguments: PersonBrowserArguments(widget.account, person));
|
||||
}
|
||||
|
||||
void _transformItems(List<Person> items) {
|
||||
_items = items
|
||||
.sorted((a, b) => a.name.compareTo(b.name))
|
||||
.map((e) => _PersonListItem(
|
||||
account: widget.account,
|
||||
name: e.name,
|
||||
faceUrl: api_util.getFacePreviewUrl(widget.account, e.thumbFaceId,
|
||||
size: k.faceThumbSize),
|
||||
onTap: () => _onItemTap(e),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _reqQuery() {
|
||||
_bloc.add(ListPersonBlocQuery(widget.account));
|
||||
}
|
||||
|
||||
late final _bloc = ListPersonBloc(KiwiContainer().resolve<DiContainer>());
|
||||
|
||||
var _items = <_ListItem>[];
|
||||
|
||||
static final _log = Logger("widget.people_browser._PeopleBrowserState");
|
||||
}
|
||||
|
||||
abstract class _ListItem {
|
||||
_ListItem({
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
Widget buildWidget(BuildContext context);
|
||||
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _PersonListItem extends _ListItem {
|
||||
_PersonListItem({
|
||||
required this.account,
|
||||
required this.name,
|
||||
required this.faceUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: _buildFaceImage(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
name + "\n",
|
||||
style: Theme.of(context).textTheme.bodyText1!.copyWith(
|
||||
color: AppTheme.getPrimaryTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFaceImage(BuildContext context) {
|
||||
Widget cover;
|
||||
try {
|
||||
cover = FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: BoxFit.cover,
|
||||
child: CachedNetworkImage(
|
||||
cacheManager: ThumbnailCacheManager.inst,
|
||||
imageUrl: faceUrl!,
|
||||
httpHeaders: {
|
||||
"Authorization": Api.getAuthorizationHeaderValue(account),
|
||||
},
|
||||
fadeInDuration: const Duration(),
|
||||
filterQuality: FilterQuality.high,
|
||||
errorWidget: (context, url, error) {
|
||||
// just leave it empty
|
||||
return Container();
|
||||
},
|
||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
cover = Icon(
|
||||
Icons.person,
|
||||
color: Colors.white.withOpacity(.8),
|
||||
size: 64,
|
||||
);
|
||||
}
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(128),
|
||||
child: Container(
|
||||
color: AppTheme.getListItemBackgroundColor(context),
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String name;
|
||||
final String? faceUrl;
|
||||
}
|
|
@ -21,6 +21,7 @@ import 'package:nc_photos/pref.dart';
|
|||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/url_launcher_util.dart';
|
||||
import 'package:nc_photos/widget/people_browser.dart';
|
||||
import 'package:nc_photos/widget/person_browser.dart';
|
||||
|
||||
class SearchLanding extends StatefulWidget {
|
||||
|
@ -110,21 +111,30 @@ class _SearchLandingState extends State<SearchLanding> {
|
|||
|
||||
List<Widget> _buildPeopleSection(
|
||||
BuildContext context, SearchLandingBlocState state) {
|
||||
final isNoResult = (state is SearchLandingBlocSuccess ||
|
||||
state is SearchLandingBlocFailure) &&
|
||||
state.persons.isEmpty;
|
||||
return [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(L10n.global().collectionPeopleLabel),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
launch(help_util.peopleUrl);
|
||||
},
|
||||
tooltip: L10n.global().helpTooltip,
|
||||
icon: const Icon(Icons.help_outline),
|
||||
),
|
||||
trailing: isNoResult
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
launch(help_util.peopleUrl);
|
||||
},
|
||||
tooltip: L10n.global().helpTooltip,
|
||||
icon: const Icon(Icons.help_outline),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(PeopleBrowser.routeName,
|
||||
arguments: PeopleBrowserArguments(widget.account));
|
||||
},
|
||||
child: Text(L10n.global().showAllButtonLabel),
|
||||
),
|
||||
),
|
||||
if ((state is SearchLandingBlocSuccess ||
|
||||
state is SearchLandingBlocFailure) &&
|
||||
state.persons.isEmpty)
|
||||
if (isNoResult)
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Center(
|
||||
|
|
Loading…
Reference in a new issue