mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28:50 +01:00
Refactor: rewrite search landing widget
This commit is contained in:
parent
0af86944d0
commit
3e5dc3fedf
9 changed files with 732 additions and 572 deletions
|
@ -1,125 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/controller/account_pref_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
import 'package:nc_photos/use_case/person/list_person.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'search_landing.g.dart';
|
||||
|
||||
abstract class SearchLandingBlocEvent {
|
||||
const SearchLandingBlocEvent();
|
||||
}
|
||||
|
||||
@toString
|
||||
class SearchLandingBlocQuery extends SearchLandingBlocEvent {
|
||||
const SearchLandingBlocQuery(this.account, this.accountPrefController);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account account;
|
||||
final AccountPrefController accountPrefController;
|
||||
}
|
||||
|
||||
@toString
|
||||
abstract class SearchLandingBlocState {
|
||||
const SearchLandingBlocState(this.account, this.persons, this.locations);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Account? account;
|
||||
final List<Person> persons;
|
||||
final LocationGroupResult locations;
|
||||
}
|
||||
|
||||
class SearchLandingBlocInit extends SearchLandingBlocState {
|
||||
SearchLandingBlocInit()
|
||||
: super(null, const [], const LocationGroupResult([], [], [], []));
|
||||
}
|
||||
|
||||
class SearchLandingBlocLoading extends SearchLandingBlocState {
|
||||
const SearchLandingBlocLoading(
|
||||
Account? account, List<Person> persons, LocationGroupResult locations)
|
||||
: super(account, persons, locations);
|
||||
}
|
||||
|
||||
class SearchLandingBlocSuccess extends SearchLandingBlocState {
|
||||
const SearchLandingBlocSuccess(
|
||||
Account? account, List<Person> persons, LocationGroupResult locations)
|
||||
: super(account, persons, locations);
|
||||
}
|
||||
|
||||
@toString
|
||||
class SearchLandingBlocFailure extends SearchLandingBlocState {
|
||||
const SearchLandingBlocFailure(Account? account, List<Person> persons,
|
||||
LocationGroupResult locations, this.exception)
|
||||
: super(account, persons, locations);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object exception;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class SearchLandingBloc
|
||||
extends Bloc<SearchLandingBlocEvent, SearchLandingBlocState> {
|
||||
SearchLandingBloc(this._c) : super(SearchLandingBlocInit()) {
|
||||
on<SearchLandingBlocEvent>(_onEvent);
|
||||
}
|
||||
|
||||
Future<void> _onEvent(SearchLandingBlocEvent event,
|
||||
Emitter<SearchLandingBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is SearchLandingBlocQuery) {
|
||||
await _onEventQuery(event, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEventQuery(
|
||||
SearchLandingBlocQuery ev, Emitter<SearchLandingBlocState> emit) async {
|
||||
try {
|
||||
emit(
|
||||
SearchLandingBlocLoading(ev.account, state.persons, state.locations));
|
||||
|
||||
List<Person>? persons;
|
||||
try {
|
||||
persons = await _queryPeople(ev);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_onEventQuery] Failed while _queryPeople", e, stackTrace);
|
||||
}
|
||||
|
||||
LocationGroupResult? locations;
|
||||
try {
|
||||
locations = await _queryLocations(ev);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_onEventQuery] Failed while _queryLocations", e, stackTrace);
|
||||
}
|
||||
|
||||
emit(SearchLandingBlocSuccess(ev.account, persons ?? [],
|
||||
locations ?? const LocationGroupResult([], [], [], [])));
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||
emit(SearchLandingBlocFailure(
|
||||
ev.account, state.persons, state.locations, e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Person>> _queryPeople(SearchLandingBlocQuery ev) =>
|
||||
ListPerson(_c.withLocalRepo())(
|
||||
ev.account, ev.accountPrefController.personProvider.value)
|
||||
.last;
|
||||
|
||||
Future<LocationGroupResult> _queryLocations(SearchLandingBlocQuery ev) =>
|
||||
ListLocationGroup(_c.withLocalRepo())(ev.account);
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search_landing.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$SearchLandingBlocNpLog on SearchLandingBloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("bloc.search_landing.SearchLandingBloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$SearchLandingBlocQueryToString on SearchLandingBlocQuery {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "SearchLandingBlocQuery {account: $account, accountPrefController: $accountPrefController}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$SearchLandingBlocStateToString on SearchLandingBlocState {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "${objectRuntimeType(this, "SearchLandingBlocState")} {account: $account, persons: [length: ${persons.length}], locations: $locations}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$SearchLandingBlocFailureToString on SearchLandingBlocFailure {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "SearchLandingBlocFailure {account: $account, persons: [length: ${persons.length}], locations: $locations, exception: $exception}";
|
||||
}
|
||||
}
|
|
@ -132,7 +132,6 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
if (_isShowLanding(state))
|
||||
SliverToBoxAdapter(
|
||||
child: SearchLanding(
|
||||
account: widget.account,
|
||||
onFavoritePressed: _onLandingFavoritePressed,
|
||||
onVideoPressed: _onLandingVideoPressed,
|
||||
),
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:copy_with/copy_with.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';
|
||||
import 'package:nc_photos/bloc_util.dart';
|
||||
import 'package:nc_photos/controller/account_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/controller/persons_controller.dart';
|
||||
import 'package:nc_photos/controller/places_controller.dart';
|
||||
import 'package:nc_photos/entity/collection/builder.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_event.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/stream_util.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/url_launcher_util.dart';
|
||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||
|
@ -25,417 +30,348 @@ import 'package:nc_photos/widget/person_thumbnail.dart';
|
|||
import 'package:nc_photos/widget/places_browser.dart';
|
||||
import 'package:nc_photos/widget/settings/account_settings.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
part 'search_landing.g.dart';
|
||||
part 'search_landing/bloc.dart';
|
||||
part 'search_landing/state_event.dart';
|
||||
part 'search_landing/type.dart';
|
||||
part 'search_landing/view.dart';
|
||||
|
||||
class SearchLanding extends StatefulWidget {
|
||||
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||
typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||
|
||||
class SearchLanding extends StatelessWidget {
|
||||
const SearchLanding({
|
||||
Key? key,
|
||||
required this.account,
|
||||
super.key,
|
||||
this.onFavoritePressed,
|
||||
this.onVideoPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _SearchLandingState();
|
||||
Widget build(BuildContext context) {
|
||||
final accountController = context.read<AccountController>();
|
||||
return BlocProvider(
|
||||
create: (_) => _Bloc(
|
||||
account: accountController.account,
|
||||
personsController: accountController.personsController,
|
||||
placesController: accountController.placesController,
|
||||
),
|
||||
child: _WrappedSearchLanding(
|
||||
onFavoritePressed: onFavoritePressed,
|
||||
onVideoPressed: onVideoPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final VoidCallback? onFavoritePressed;
|
||||
final VoidCallback? onVideoPressed;
|
||||
}
|
||||
|
||||
class _WrappedSearchLanding extends StatefulWidget {
|
||||
const _WrappedSearchLanding({
|
||||
this.onFavoritePressed,
|
||||
this.onVideoPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _WrappedSearchLandingState();
|
||||
|
||||
final Account account;
|
||||
final VoidCallback? onFavoritePressed;
|
||||
final VoidCallback? onVideoPressed;
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _SearchLandingState extends State<SearchLanding> {
|
||||
class _WrappedSearchLandingState extends State<_WrappedSearchLanding> {
|
||||
@override
|
||||
initState() {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initBloc();
|
||||
_bloc
|
||||
..add(const _LoadPersons())
|
||||
..add(const _LoadPlaces());
|
||||
}
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return BlocListener<SearchLandingBloc, SearchLandingBlocState>(
|
||||
bloc: _bloc,
|
||||
listener: (context, state) => _onStateChange(context, state),
|
||||
child: BlocBuilder<SearchLandingBloc, SearchLandingBlocState>(
|
||||
bloc: _bloc,
|
||||
builder: (context, state) => _buildContent(context, state),
|
||||
Widget build(BuildContext context) {
|
||||
return VisibilityDetector(
|
||||
key: _key,
|
||||
onVisibilityChanged: (info) {
|
||||
final isVisible = info.visibleFraction >= 0.2;
|
||||
if (isVisible != _isVisible) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVisible = isVisible;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
_BlocListener(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.persons != current.persons,
|
||||
listener: (context, state) {
|
||||
_bloc.add(_TransformPersonItems(state.persons));
|
||||
},
|
||||
),
|
||||
_BlocListener(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.places != current.places,
|
||||
listener: (context, state) {
|
||||
_bloc.add(_TransformPlaceItems(state.places));
|
||||
},
|
||||
),
|
||||
_BlocListener(
|
||||
listenWhen: (previous, current) => previous.error != current.error,
|
||||
listener: (context, state) {
|
||||
if (state.error != null && _isVisible == true) {
|
||||
if (state.error is ApiException) {
|
||||
final e = state.error as ApiException;
|
||||
if (e.response.statusCode == 404) {
|
||||
// face recognition app probably not installed, ignore
|
||||
return;
|
||||
}
|
||||
}
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(exception_util.toUserString(state.error!.error)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
ValueStreamBuilder<PersonProvider>(
|
||||
stream: context
|
||||
.read<AccountController>()
|
||||
.accountPrefController
|
||||
.personProvider,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.requireData == PersonProvider.none) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return const _PeopleSection();
|
||||
}
|
||||
},
|
||||
),
|
||||
const _PlaceSection(),
|
||||
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: widget.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: widget.onVideoPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is SearchLandingBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
_reqQuery();
|
||||
} else {
|
||||
// process the current state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_onStateChange(context, _bloc.state);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
late final _bloc = context.read<_Bloc>();
|
||||
|
||||
Widget _buildContent(BuildContext context, SearchLandingBlocState state) {
|
||||
final _key = GlobalKey();
|
||||
bool? _isVisible;
|
||||
}
|
||||
|
||||
class _PeopleSection extends StatelessWidget {
|
||||
const _PeopleSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (context
|
||||
.read<AccountController>()
|
||||
.accountPrefController
|
||||
.personProvider
|
||||
.value !=
|
||||
PersonProvider.none)
|
||||
..._buildPeopleSection(context, state),
|
||||
..._buildLocationSection(context, state),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(L10n.global().categoriesLabel),
|
||||
title: Text(L10n.global().collectionPeopleLabel),
|
||||
trailing: _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.transformedPersonItems !=
|
||||
current.transformedPersonItems,
|
||||
builder: (context, state) => state.transformedPersonItems.isEmpty
|
||||
? 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: () {
|
||||
Navigator.of(context).pushNamed(PeopleBrowser.routeName);
|
||||
},
|
||||
child: Text(L10n.global().showAllButtonLabel),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
_BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isPersonsLoading != current.isPersonsLoading ||
|
||||
previous.transformedPersonItems != current.transformedPersonItems,
|
||||
builder: (context, state) {
|
||||
if (state.isPersonsLoading) {
|
||||
return const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
} else {
|
||||
if (state.transformedPersonItems.isEmpty) {
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Center(
|
||||
child:
|
||||
Text(L10n.global().searchLandingPeopleListEmptyText2),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 128,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.transformedPersonItems.length,
|
||||
itemBuilder: (context, i) {
|
||||
final item = state.transformedPersonItems[i];
|
||||
return _PersonItemView(
|
||||
account: context.read<_Bloc>().account,
|
||||
item: item,
|
||||
onTap: () => _onItemTap(context, item.person),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPeopleSection(
|
||||
BuildContext context, SearchLandingBlocState state) {
|
||||
final isNoResult = (state is SearchLandingBlocSuccess ||
|
||||
state is SearchLandingBlocFailure) &&
|
||||
_personItems.isEmpty;
|
||||
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: () {
|
||||
Navigator.of(context).pushNamed(PeopleBrowser.routeName);
|
||||
},
|
||||
child: Text(L10n.global().showAllButtonLabel),
|
||||
),
|
||||
),
|
||||
if (isNoResult)
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Center(
|
||||
child: Text(L10n.global().searchLandingPeopleListEmptyText2),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 128,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _personItems.length,
|
||||
itemBuilder: (context, i) => _personItems[i].buildWidget(context),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
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),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onStateChange(BuildContext context, SearchLandingBlocState state) {
|
||||
if (state is SearchLandingBlocInit) {
|
||||
_personItems = [];
|
||||
_locationItems = [];
|
||||
} else if (state is SearchLandingBlocSuccess ||
|
||||
state is SearchLandingBlocLoading) {
|
||||
_transformItems(state.persons, state.locations);
|
||||
} else if (state is SearchLandingBlocFailure) {
|
||||
_transformItems(state.persons, state.locations);
|
||||
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();
|
||||
}
|
||||
|
||||
void _onPersonItemTap(Person person) {
|
||||
void _onItemTap(BuildContext context, Person person) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
CollectionBrowser.routeName,
|
||||
arguments: CollectionBrowserArguments(
|
||||
CollectionBuilder.byPerson(widget.account, person),
|
||||
CollectionBuilder.byPerson(context.read<_Bloc>().account, person),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onLocationItemTap(LocationGroup location) {
|
||||
class _PlaceSection extends StatelessWidget {
|
||||
const _PlaceSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(L10n.global().collectionPlacesLabel),
|
||||
trailing: _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.transformedPlaceItems != current.transformedPlaceItems,
|
||||
builder: (context, state) => state.transformedPlaceItems.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(PlacesBrowser.routeName);
|
||||
},
|
||||
child: Text(L10n.global().showAllButtonLabel),
|
||||
),
|
||||
),
|
||||
),
|
||||
_BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isPlacesLoading != current.isPlacesLoading ||
|
||||
previous.transformedPlaceItems != current.transformedPlaceItems,
|
||||
builder: (context, state) {
|
||||
if (state.isPlacesLoading) {
|
||||
return const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
} else {
|
||||
if (state.transformedPlaceItems.isEmpty) {
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Center(
|
||||
child: Text(L10n.global().listNoResultsText),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 128,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.transformedPlaceItems.length,
|
||||
itemBuilder: (context, i) {
|
||||
final item = state.transformedPlaceItems[i];
|
||||
return _PlaceItemView(
|
||||
account: context.read<_Bloc>().account,
|
||||
item: item,
|
||||
onTap: () => _onItemTap(context, item.place),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onItemTap(BuildContext context, LocationGroup place) {
|
||||
Navigator.of(context).pushNamed(
|
||||
CollectionBrowser.routeName,
|
||||
arguments: CollectionBrowserArguments(
|
||||
CollectionBuilder.byLocationGroup(widget.account, location),
|
||||
CollectionBuilder.byLocationGroup(context.read<_Bloc>().account, place),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _transformItems(List<Person> persons, LocationGroupResult locations) {
|
||||
_transformPersons(persons);
|
||||
_transformLocations(locations);
|
||||
}
|
||||
|
||||
void _transformPersons(List<Person> persons) {
|
||||
_personItems = persons
|
||||
.sorted((a, b) {
|
||||
final countCompare = (b.count ?? 0).compareTo(a.count ?? 0);
|
||||
if (countCompare == 0) {
|
||||
return a.name.compareTo(b.name);
|
||||
} else {
|
||||
return countCompare;
|
||||
}
|
||||
})
|
||||
.take(10)
|
||||
.map((e) => _LandingPersonItem(
|
||||
account: widget.account,
|
||||
person: e,
|
||||
onTap: () => _onPersonItemTap(e),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _transformLocations(LocationGroupResult locations) {
|
||||
_locationItems = locations.name
|
||||
.sorted((a, b) {
|
||||
final compare = b.count.compareTo(a.count);
|
||||
if (compare == 0) {
|
||||
return a.place.compareTo(b.place);
|
||||
} else {
|
||||
return compare;
|
||||
}
|
||||
})
|
||||
.take(10)
|
||||
.map((e) => _LandingLocationItem(
|
||||
account: widget.account,
|
||||
name: e.place,
|
||||
thumbUrl: NetworkRectThumbnail.imageUrlForFileId(
|
||||
widget.account, e.latestFileId),
|
||||
onTap: () => _onLocationItemTap(e),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _reqQuery() {
|
||||
_bloc.add(SearchLandingBlocQuery(widget.account, _accountPrefController));
|
||||
}
|
||||
|
||||
late final _bloc = SearchLandingBloc(KiwiContainer().resolve<DiContainer>());
|
||||
late final _accountPrefController =
|
||||
context.read<AccountController>().accountPrefController;
|
||||
|
||||
var _personItems = <_LandingPersonItem>[];
|
||||
var _locationItems = <_LandingLocationItem>[];
|
||||
}
|
||||
|
||||
class _LandingPersonWidget extends StatelessWidget {
|
||||
const _LandingPersonWidget({
|
||||
required this.account,
|
||||
required this.person,
|
||||
required this.label,
|
||||
required this.coverUrl,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(72 / 2),
|
||||
child: PersonThumbnail(
|
||||
dimension: 72,
|
||||
account: account,
|
||||
person: person,
|
||||
coverUrl: coverUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _Label(label: label)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final Person person;
|
||||
final String label;
|
||||
final String? coverUrl;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _LandingLocationWidget extends StatelessWidget {
|
||||
const _LandingLocationWidget({
|
||||
required this.account,
|
||||
required this.label,
|
||||
required this.coverUrl,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: _LocationCoverImage(
|
||||
dimension: 72,
|
||||
account: account,
|
||||
coverUrl: coverUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _Label(label: label)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String label;
|
||||
final String? coverUrl;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String label;
|
||||
}
|
||||
|
|
|
@ -2,13 +2,126 @@
|
|||
|
||||
part of 'search_landing.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $_StateCopyWithWorker {
|
||||
_State call(
|
||||
{List<Person>? persons,
|
||||
bool? isPersonsLoading,
|
||||
List<_PersonItem>? transformedPersonItems,
|
||||
LocationGroupResult? places,
|
||||
bool? isPlacesLoading,
|
||||
List<_PlaceItem>? transformedPlaceItems,
|
||||
ExceptionEvent? error});
|
||||
}
|
||||
|
||||
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||
_$_StateCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
_State call(
|
||||
{dynamic persons,
|
||||
dynamic isPersonsLoading,
|
||||
dynamic transformedPersonItems,
|
||||
dynamic places,
|
||||
dynamic isPlacesLoading,
|
||||
dynamic transformedPlaceItems,
|
||||
dynamic error = copyWithNull}) {
|
||||
return _State(
|
||||
persons: persons as List<Person>? ?? that.persons,
|
||||
isPersonsLoading: isPersonsLoading as bool? ?? that.isPersonsLoading,
|
||||
transformedPersonItems: transformedPersonItems as List<_PersonItem>? ??
|
||||
that.transformedPersonItems,
|
||||
places: places as LocationGroupResult? ?? that.places,
|
||||
isPlacesLoading: isPlacesLoading as bool? ?? that.isPlacesLoading,
|
||||
transformedPlaceItems: transformedPlaceItems as List<_PlaceItem>? ??
|
||||
that.transformedPlaceItems,
|
||||
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
|
||||
}
|
||||
|
||||
final _State that;
|
||||
}
|
||||
|
||||
extension $_StateCopyWith on _State {
|
||||
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_SearchLandingStateNpLog on _SearchLandingState {
|
||||
extension _$_WrappedSearchLandingStateNpLog on _WrappedSearchLandingState {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.search_landing._SearchLandingState");
|
||||
static final log = Logger("widget.search_landing._WrappedSearchLandingState");
|
||||
}
|
||||
|
||||
extension _$_BlocNpLog on _Bloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.search_landing._Bloc");
|
||||
}
|
||||
|
||||
extension _$_PersonItemNpLog on _PersonItem {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.search_landing._PersonItem");
|
||||
}
|
||||
|
||||
extension _$_PlaceItemNpLog on _PlaceItem {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.search_landing._PlaceItem");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_State {persons: [length: ${persons.length}], isPersonsLoading: $isPersonsLoading, transformedPersonItems: [length: ${transformedPersonItems.length}], places: $places, isPlacesLoading: $isPlacesLoading, transformedPlaceItems: [length: ${transformedPlaceItems.length}], error: $error}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_LoadPersonsToString on _LoadPersons {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_LoadPersons {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_TransformPersonItemsToString on _TransformPersonItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_TransformPersonItems {persons: [length: ${persons.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_LoadPlacesToString on _LoadPlaces {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_LoadPlaces {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_TransformPlaceItemsToString on _TransformPlaceItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_TransformPlaceItems {places: $places}";
|
||||
}
|
||||
}
|
||||
|
|
94
app/lib/widget/search_landing/bloc.dart
Normal file
94
app/lib/widget/search_landing/bloc.dart
Normal file
|
@ -0,0 +1,94 @@
|
|||
part of '../search_landing.dart';
|
||||
|
||||
@npLog
|
||||
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||
_Bloc({
|
||||
required this.account,
|
||||
required this.personsController,
|
||||
required this.placesController,
|
||||
}) : super(_State.init()) {
|
||||
on<_LoadPersons>(_onLoadPersons);
|
||||
on<_TransformPersonItems>(_onTransformPersonItems);
|
||||
on<_LoadPlaces>(_onLoadPlaces);
|
||||
on<_TransformPlaceItems>(_onTransformPlaceItems);
|
||||
}
|
||||
|
||||
@override
|
||||
String get tag => _log.fullName;
|
||||
|
||||
Future<void> _onLoadPersons(_LoadPersons ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
return emit.forEach<PersonStreamEvent>(
|
||||
personsController.stream,
|
||||
onData: (data) => state.copyWith(
|
||||
persons: data.data,
|
||||
isPersonsLoading: data.hasNext,
|
||||
),
|
||||
onError: (e, stackTrace) {
|
||||
_log.severe("[_onLoadPersons] Uncaught exception", e, stackTrace);
|
||||
return state.copyWith(
|
||||
isPersonsLoading: false,
|
||||
error: ExceptionEvent(e, stackTrace),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTransformPersonItems(
|
||||
_TransformPersonItems ev, Emitter<_State> emit) async {
|
||||
_log.info(ev);
|
||||
final transformed = ev.persons
|
||||
.sorted((a, b) {
|
||||
final countCompare = (b.count ?? 0).compareTo(a.count ?? 0);
|
||||
if (countCompare == 0) {
|
||||
return a.name.compareTo(b.name);
|
||||
} else {
|
||||
return countCompare;
|
||||
}
|
||||
})
|
||||
.take(10)
|
||||
.map(_PersonItem.new)
|
||||
.toList();
|
||||
emit(state.copyWith(transformedPersonItems: transformed));
|
||||
}
|
||||
|
||||
Future<void> _onLoadPlaces(_LoadPlaces ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
return emit.forEach<PlaceStreamEvent>(
|
||||
placesController.stream,
|
||||
onData: (data) => state.copyWith(
|
||||
places: data.data,
|
||||
isPlacesLoading: data.hasNext,
|
||||
),
|
||||
onError: (e, stackTrace) {
|
||||
_log.severe("[_onLoadPlaces] Uncaught exception", e, stackTrace);
|
||||
return state.copyWith(
|
||||
isPlacesLoading: false,
|
||||
error: ExceptionEvent(e, stackTrace),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTransformPlaceItems(
|
||||
_TransformPlaceItems ev, Emitter<_State> emit) async {
|
||||
_log.info(ev);
|
||||
final transformed = ev.places.name
|
||||
.sorted((a, b) {
|
||||
final compare = b.count.compareTo(a.count);
|
||||
if (compare == 0) {
|
||||
return a.place.compareTo(b.place);
|
||||
} else {
|
||||
return compare;
|
||||
}
|
||||
})
|
||||
.take(10)
|
||||
.map((e) => _PlaceItem(account: account, place: e))
|
||||
.toList();
|
||||
emit(state.copyWith(transformedPlaceItems: transformed));
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final PersonsController personsController;
|
||||
final PlacesController placesController;
|
||||
}
|
77
app/lib/widget/search_landing/state_event.dart
Normal file
77
app/lib/widget/search_landing/state_event.dart
Normal file
|
@ -0,0 +1,77 @@
|
|||
part of '../search_landing.dart';
|
||||
|
||||
@genCopyWith
|
||||
@toString
|
||||
class _State {
|
||||
const _State({
|
||||
required this.persons,
|
||||
required this.isPersonsLoading,
|
||||
required this.transformedPersonItems,
|
||||
required this.places,
|
||||
required this.isPlacesLoading,
|
||||
required this.transformedPlaceItems,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory _State.init() => const _State(
|
||||
persons: [],
|
||||
isPersonsLoading: false,
|
||||
transformedPersonItems: [],
|
||||
places: LocationGroupResult([], [], [], []),
|
||||
isPlacesLoading: false,
|
||||
transformedPlaceItems: [],
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<Person> persons;
|
||||
final bool isPersonsLoading;
|
||||
final List<_PersonItem> transformedPersonItems;
|
||||
final LocationGroupResult places;
|
||||
final bool isPlacesLoading;
|
||||
final List<_PlaceItem> transformedPlaceItems;
|
||||
|
||||
final ExceptionEvent? error;
|
||||
}
|
||||
|
||||
abstract class _Event {}
|
||||
|
||||
/// Load the list of [Person]s belonging to this account
|
||||
@toString
|
||||
class _LoadPersons implements _Event {
|
||||
const _LoadPersons();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _TransformPersonItems implements _Event {
|
||||
const _TransformPersonItems(this.persons);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<Person> persons;
|
||||
}
|
||||
|
||||
/// Load the location groups belonging to this account
|
||||
@toString
|
||||
class _LoadPlaces implements _Event {
|
||||
const _LoadPlaces();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
/// Transform the location groups (e.g., filtering, sorting, etc)
|
||||
@toString
|
||||
class _TransformPlaceItems implements _Event {
|
||||
const _TransformPlaceItems(this.places);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final LocationGroupResult places;
|
||||
}
|
|
@ -1,49 +1,46 @@
|
|||
part of '../search_landing.dart';
|
||||
|
||||
class _LandingPersonItem {
|
||||
_LandingPersonItem({
|
||||
required this.account,
|
||||
required this.person,
|
||||
this.onTap,
|
||||
}) : name = person.name,
|
||||
faceUrl = person.getCoverUrl(
|
||||
k.photoLargeSize,
|
||||
k.photoLargeSize,
|
||||
isKeepAspectRatio: true,
|
||||
);
|
||||
|
||||
Widget buildWidget(BuildContext context) => _LandingPersonWidget(
|
||||
account: account,
|
||||
person: person,
|
||||
label: name,
|
||||
coverUrl: faceUrl,
|
||||
onTap: onTap,
|
||||
@npLog
|
||||
class _PersonItem {
|
||||
_PersonItem(this.person) {
|
||||
try {
|
||||
_coverUrl = person.getCoverUrl(
|
||||
k.photoLargeSize,
|
||||
k.photoLargeSize,
|
||||
isKeepAspectRatio: true,
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("[_PersonItem] Failed while getCoverUrl", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
String get name => person.name;
|
||||
String? get coverUrl => _coverUrl;
|
||||
|
||||
final Account account;
|
||||
final Person person;
|
||||
final String name;
|
||||
final String? faceUrl;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
String? _coverUrl;
|
||||
}
|
||||
|
||||
class _LandingLocationItem {
|
||||
const _LandingLocationItem({
|
||||
required this.account,
|
||||
required this.name,
|
||||
required this.thumbUrl,
|
||||
this.onTap,
|
||||
});
|
||||
@npLog
|
||||
class _PlaceItem {
|
||||
_PlaceItem({
|
||||
required Account account,
|
||||
required this.place,
|
||||
}) {
|
||||
try {
|
||||
_coverUrl =
|
||||
NetworkRectThumbnail.imageUrlForFileId(account, place.latestFileId);
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning(
|
||||
"[_PlaceItem] Failed while imageUrlForFileId", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildWidget(BuildContext context) => _LandingLocationWidget(
|
||||
account: account,
|
||||
label: name,
|
||||
coverUrl: thumbUrl,
|
||||
onTap: onTap,
|
||||
);
|
||||
String get name => place.place;
|
||||
String? get coverUrl => _coverUrl;
|
||||
|
||||
final Account account;
|
||||
final String name;
|
||||
final String thumbUrl;
|
||||
final VoidCallback? onTap;
|
||||
final LocationGroup place;
|
||||
|
||||
String? _coverUrl;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,113 @@
|
|||
part of '../search_landing.dart';
|
||||
|
||||
class _PersonItemView extends StatelessWidget {
|
||||
const _PersonItemView({
|
||||
required this.account,
|
||||
required this.item,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(72 / 2),
|
||||
child: PersonThumbnail(
|
||||
dimension: 72,
|
||||
account: account,
|
||||
person: item.person,
|
||||
coverUrl: item.coverUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _LabelView(label: item.name)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final _PersonItem item;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _PlaceItemView extends StatelessWidget {
|
||||
const _PlaceItemView({
|
||||
required this.account,
|
||||
required this.item,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: _LocationCoverImage(
|
||||
dimension: 72,
|
||||
account: account,
|
||||
coverUrl: item.coverUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _LabelView(label: item.name)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final _PlaceItem item;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _LabelView extends StatelessWidget {
|
||||
const _LabelView({
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String label;
|
||||
}
|
||||
|
||||
class _LocationCoverPlaceholder extends StatelessWidget {
|
||||
const _LocationCoverPlaceholder();
|
||||
|
||||
|
|
Loading…
Reference in a new issue