nc-photos/app/lib/widget/search_landing.dart

443 lines
13 KiB
Dart
Raw Normal View History

2022-08-06 06:21:11 +02:00
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/search_landing.dart';
2023-06-10 12:44:02 +02:00
import 'package:nc_photos/controller/account_controller.dart';
2022-08-06 06:21:11 +02:00
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/collection/builder.dart';
2022-08-06 06:21:11 +02:00
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/help_utils.dart' as help_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/url_launcher_util.dart';
2022-08-28 17:35:29 +02:00
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:nc_photos/widget/collection_browser.dart';
2022-12-18 07:20:51 +01:00
import 'package:nc_photos/widget/network_thumbnail.dart';
import 'package:nc_photos/widget/people_browser.dart';
2023-07-22 20:57:01 +02:00
import 'package:nc_photos/widget/person_thumbnail.dart';
2022-08-28 17:35:29 +02:00
import 'package:nc_photos/widget/places_browser.dart';
import 'package:nc_photos/widget/settings/account_settings.dart';
2022-12-16 16:01:04 +01:00
import 'package:np_codegen/np_codegen.dart';
part 'search_landing.g.dart';
2023-07-16 13:30:23 +02:00
part 'search_landing/type.dart';
part 'search_landing/view.dart';
2022-08-06 06:21:11 +02:00
class SearchLanding extends StatefulWidget {
const SearchLanding({
Key? key,
required this.account,
this.onFavoritePressed,
this.onVideoPressed,
}) : super(key: key);
@override
createState() => _SearchLandingState();
final Account account;
final VoidCallback? onFavoritePressed;
final VoidCallback? onVideoPressed;
}
2022-12-16 16:01:04 +01:00
@npLog
2022-08-06 06:21:11 +02:00
class _SearchLandingState extends State<SearchLanding> {
@override
initState() {
super.initState();
_initBloc();
}
@override
build(BuildContext context) {
return BlocListener<SearchLandingBloc, SearchLandingBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<SearchLandingBloc, SearchLandingBlocState>(
bloc: _bloc,
2022-11-12 10:55:33 +01:00
builder: (context, state) => _buildContent(context, state),
2022-08-06 06:21:11 +02:00
),
);
}
void _initBloc() {
if (_bloc.state is SearchLandingBlocInit) {
_log.info("[_initBloc] Initialize bloc");
_reqQuery();
} else {
// process the current state
WidgetsBinding.instance.addPostFrameCallback((_) {
2022-12-11 06:01:13 +01:00
if (mounted) {
setState(() {
_onStateChange(context, _bloc.state);
});
}
2022-08-06 06:21:11 +02:00
});
}
}
Widget _buildContent(BuildContext context, SearchLandingBlocState state) {
return Column(
children: [
2023-06-10 12:44:02 +02:00
if (context
.read<AccountController>()
.accountPrefController
.personProvider
.value !=
PersonProvider.none)
2022-08-06 06:21:11 +02:00
..._buildPeopleSection(context, state),
2022-08-28 17:35:29 +02:00
..._buildLocationSection(context, state),
2022-08-06 06:21:11 +02:00
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(L10n.global().categoriesLabel),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: const Icon(Icons.star_border),
title: Text(L10n.global().collectionFavoritesLabel),
onTap: _onFavoritePressed,
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Divider(height: 1),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: const Icon(Icons.ondemand_video_outlined),
title: Text(L10n.global().searchLandingCategoryVideosLabel),
onTap: _onVideoPressed,
),
],
);
}
List<Widget> _buildPeopleSection(
BuildContext context, SearchLandingBlocState state) {
final isNoResult = (state is SearchLandingBlocSuccess ||
state is SearchLandingBlocFailure) &&
2022-08-28 17:35:29 +02:00
_personItems.isEmpty;
2022-08-06 06:21:11 +02:00
return [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(L10n.global().collectionPeopleLabel),
trailing: isNoResult
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pushNamed(
AccountSettings.routeName,
arguments: const AccountSettingsArguments(
highlight: AccountSettingsOption.personProvider,
),
);
},
tooltip: L10n.global().accountSettingsTooltip,
icon: const Icon(Icons.settings_outlined),
),
IconButton(
onPressed: () {
launch(help_util.peopleUrl);
},
tooltip: L10n.global().helpTooltip,
icon: const Icon(Icons.help_outline),
),
],
)
: TextButton(
onPressed: () {
2023-07-03 19:23:42 +02:00
Navigator.of(context).pushNamed(PeopleBrowser.routeName);
},
child: Text(L10n.global().showAllButtonLabel),
),
2022-08-06 06:21:11 +02:00
),
if (isNoResult)
2022-08-06 06:21:11 +02:00
SizedBox(
height: 48,
2023-07-24 15:48:35 +02:00
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Center(
child: Text(L10n.global().searchLandingPeopleListEmptyText2),
),
2022-08-06 06:21:11 +02:00
),
)
else
SizedBox(
height: 128,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
2022-08-28 17:35:29 +02:00
itemCount: _personItems.length,
itemBuilder: (context, i) => _personItems[i].buildWidget(context),
2022-08-06 06:21:11 +02:00
),
),
];
}
2022-08-28 17:35:29 +02:00
List<Widget> _buildLocationSection(
BuildContext context, SearchLandingBlocState state) {
final isNoResult = (state is SearchLandingBlocSuccess ||
state is SearchLandingBlocFailure) &&
_locationItems.isEmpty;
return [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(L10n.global().collectionPlacesLabel),
trailing: isNoResult
? null
: TextButton(
onPressed: () {
Navigator.of(context).pushNamed(PlacesBrowser.routeName,
arguments: PlacesBrowserArguments(widget.account));
},
child: Text(L10n.global().showAllButtonLabel),
),
),
if (isNoResult)
SizedBox(
height: 48,
child: Center(
child: Text(L10n.global().listNoResultsText),
),
)
else
SizedBox(
height: 128,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: _locationItems.length,
itemBuilder: (context, i) => _locationItems[i].buildWidget(context),
),
),
];
2022-08-06 06:21:11 +02:00
}
void _onStateChange(BuildContext context, SearchLandingBlocState state) {
if (state is SearchLandingBlocInit) {
2022-08-28 17:35:29 +02:00
_personItems = [];
_locationItems = [];
2022-08-06 06:21:11 +02:00
} else if (state is SearchLandingBlocSuccess ||
state is SearchLandingBlocLoading) {
2022-08-28 17:35:29 +02:00
_transformItems(state.persons, state.locations);
2022-08-06 06:21:11 +02:00
} else if (state is SearchLandingBlocFailure) {
2022-08-28 17:35:29 +02:00
_transformItems(state.persons, state.locations);
2022-08-06 06:21:11 +02:00
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 _onFavoritePressed() {
widget.onFavoritePressed?.call();
}
void _onVideoPressed() {
widget.onVideoPressed?.call();
}
2022-08-28 17:35:29 +02:00
void _onPersonItemTap(Person person) {
Navigator.pushNamed(
context,
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(
CollectionBuilder.byPerson(widget.account, person),
),
);
2022-08-06 06:21:11 +02:00
}
2022-08-28 17:35:29 +02:00
void _onLocationItemTap(LocationGroup location) {
Navigator.of(context).pushNamed(
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(
CollectionBuilder.byLocationGroup(widget.account, location),
),
2022-08-28 17:35:29 +02:00
);
}
void _transformItems(List<Person> persons, LocationGroupResult locations) {
_transformPersons(persons);
_transformLocations(locations);
}
void _transformPersons(List<Person> persons) {
_personItems = persons
.sorted((a, b) {
2023-07-03 19:23:42 +02:00
final countCompare = (b.count ?? 0).compareTo(a.count ?? 0);
if (countCompare == 0) {
return a.name.compareTo(b.name);
} else {
return countCompare;
}
})
.take(10)
2022-08-06 06:21:11 +02:00
.map((e) => _LandingPersonItem(
account: widget.account,
2023-07-16 13:30:23 +02:00
person: e,
2022-08-28 17:35:29 +02:00
onTap: () => _onPersonItemTap(e),
))
.toList();
}
void _transformLocations(LocationGroupResult locations) {
_locationItems = locations.name
.sorted((a, b) {
2022-08-28 19:43:32 +02:00
final compare = b.count.compareTo(a.count);
if (compare == 0) {
2022-08-28 17:35:29 +02:00
return a.place.compareTo(b.place);
} else {
2022-08-28 19:43:32 +02:00
return compare;
2022-08-28 17:35:29 +02:00
}
})
.take(10)
2022-08-28 17:35:29 +02:00
.map((e) => _LandingLocationItem(
account: widget.account,
name: e.place,
thumbUrl: NetworkRectThumbnail.imageUrlForFileId(
widget.account, e.latestFileId),
2022-08-28 17:35:29 +02:00
onTap: () => _onLocationItemTap(e),
2022-08-06 06:21:11 +02:00
))
.toList();
}
void _reqQuery() {
2023-07-03 19:23:42 +02:00
_bloc.add(SearchLandingBlocQuery(widget.account, _accountPrefController));
2022-08-06 06:21:11 +02:00
}
late final _bloc = SearchLandingBloc(KiwiContainer().resolve<DiContainer>());
2023-07-03 19:23:42 +02:00
late final _accountPrefController =
context.read<AccountController>().accountPrefController;
2022-08-06 06:21:11 +02:00
2022-08-28 17:35:29 +02:00
var _personItems = <_LandingPersonItem>[];
var _locationItems = <_LandingLocationItem>[];
2022-08-06 06:21:11 +02:00
}
2023-07-16 13:30:23 +02:00
class _LandingPersonWidget extends StatelessWidget {
const _LandingPersonWidget({
2022-08-06 06:21:11 +02:00
required this.account,
2023-07-16 13:30:23 +02:00
required this.person,
required this.label,
required this.coverUrl,
2022-08-28 17:35:29 +02:00
this.onTap,
});
2023-07-16 13:30:23 +02:00
@override
Widget build(BuildContext context) {
final content = Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
2023-07-22 20:57:01 +02:00
child: ClipRRect(
borderRadius: BorderRadius.circular(72 / 2),
child: PersonThumbnail(
dimension: 72,
account: account,
person: person,
coverUrl: coverUrl,
),
2023-07-16 13:30:23 +02:00
),
),
const SizedBox(height: 8),
Expanded(child: _Label(label: label)),
],
),
);
if (onTap != null) {
return InkWell(
2022-08-28 17:35:29 +02:00
onTap: onTap,
2023-07-16 13:30:23 +02:00
child: content,
2022-08-28 17:35:29 +02:00
);
2023-07-16 13:30:23 +02:00
} else {
return content;
}
}
2022-08-28 17:35:29 +02:00
final Account account;
2023-07-16 13:30:23 +02:00
final Person person;
final String label;
final String? coverUrl;
2022-08-28 17:35:29 +02:00
final VoidCallback? onTap;
}
2023-07-16 13:30:23 +02:00
class _LandingLocationWidget extends StatelessWidget {
const _LandingLocationWidget({
2022-08-28 16:12:04 +02:00
required this.account,
required this.label,
required this.coverUrl,
this.onTap,
2023-07-16 13:30:23 +02:00
});
2022-08-28 16:12:04 +02:00
@override
2023-07-16 13:30:23 +02:00
Widget build(BuildContext context) {
2022-08-06 06:21:11 +02:00
final content = Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
2023-07-16 13:30:23 +02:00
Center(
child: _LocationCoverImage(
dimension: 72,
account: account,
coverUrl: coverUrl,
2022-08-06 06:21:11 +02:00
),
),
const SizedBox(height: 8),
2023-07-16 13:30:23 +02:00
Expanded(child: _Label(label: label)),
2022-08-06 06:21:11 +02:00
],
),
);
if (onTap != null) {
return InkWell(
onTap: onTap,
child: content,
);
} else {
return content;
}
}
2023-07-16 13:30:23 +02:00
final Account account;
final String label;
final String? coverUrl;
final VoidCallback? onTap;
}
2022-08-06 06:21:11 +02:00
2023-07-16 13:30:23 +02:00
class _Label extends StatelessWidget {
const _Label({
required this.label,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 88,
child: Text(
label + "\n",
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
2022-08-06 06:21:11 +02:00
),
);
}
2022-08-28 16:12:04 +02:00
final String label;
2022-08-06 06:21:11 +02:00
}