nc-photos/app/lib/widget/places_browser.dart
2023-07-23 03:08:00 +08:00

377 lines
10 KiB
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/app_localizations.dart';
import 'package:nc_photos/bloc/list_location.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/collection/builder.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/use_case/list_location_group.dart';
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/collection_list_item.dart';
import 'package:nc_photos/widget/network_thumbnail.dart';
import 'package:np_codegen/np_codegen.dart';
part 'places_browser.g.dart';
class PlacesBrowserArguments {
const PlacesBrowserArguments(this.account);
final Account account;
}
/// Show a list of all people associated with this account
class PlacesBrowser extends StatefulWidget {
static const routeName = "/places-browser";
static Route buildRoute(PlacesBrowserArguments args) => MaterialPageRoute(
builder: (context) => PlacesBrowser.fromArgs(args),
);
const PlacesBrowser({
Key? key,
required this.account,
}) : super(key: key);
PlacesBrowser.fromArgs(PlacesBrowserArguments args, {Key? key})
: this(
key: key,
account: args.account,
);
@override
createState() => _PlacesBrowserState();
final Account account;
}
@npLog
class _PlacesBrowserState extends State<PlacesBrowser> {
@override
initState() {
super.initState();
_initBloc();
}
@override
build(BuildContext context) {
return Scaffold(
body: BlocListener<ListLocationBloc, ListLocationBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ListLocationBloc, ListLocationBlocState>(
bloc: _bloc,
builder: (context, state) => _buildContent(context, state),
),
),
);
}
void _initBloc() {
if (_bloc.state is ListLocationBlocInit) {
_log.info("[_initBloc] Initialize bloc");
} else {
// process the current state
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_onStateChange(context, _bloc.state);
});
}
});
}
_reqQuery();
}
Widget _buildContent(BuildContext context, ListLocationBlocState state) {
return Stack(
children: [
CustomScrollView(
slivers: [
_buildAppBar(context),
if (state is ListLocationBlocLoading)
const SliverToBoxAdapter(
child: Align(
alignment: Alignment.center,
child: LinearProgressIndicator(),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _countryItems.length,
itemBuilder: (context, i) =>
_countryItems[i].buildWidget(context),
separatorBuilder: (_, __) => const SizedBox(width: 8),
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 8),
),
SliverStaggeredGrid.extentBuilder(
maxCrossAxisExtent: 160,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
itemCount: _placeItems.length,
itemBuilder: (context, i) => _placeItems[i].buildWidget(context),
staggeredTileBuilder: (_) => const StaggeredTile.count(1, 1),
),
],
),
],
);
}
Widget _buildAppBar(BuildContext context) {
return SliverAppBar(
title: Text(L10n.global().collectionPlacesLabel),
floating: true,
actions: [
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (_) => const AboutGeocodingDialog(),
);
},
icon: const Icon(Icons.info_outline),
),
],
);
}
void _onStateChange(BuildContext context, ListLocationBlocState state) {
if (state is ListLocationBlocInit) {
_placeItems = [];
_countryItems = [];
} else if (state is ListLocationBlocSuccess ||
state is ListLocationBlocLoading) {
_transformItems(state.result);
} else if (state is ListLocationBlocFailure) {
_transformItems(state.result);
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 _onLocationTap(LocationGroup location) {
Navigator.pushNamed(
context,
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(
CollectionBuilder.byLocationGroup(widget.account, location),
),
);
}
void _transformItems(LocationGroupResult? result) {
if (result == null) {
_placeItems = [];
_countryItems = [];
return;
}
int sorter(LocationGroup a, LocationGroup b) {
final compare = b.count.compareTo(a.count);
if (compare == 0) {
return a.place.compareTo(b.place);
} else {
return compare;
}
}
_placeItems = result.name
.sorted(sorter)
.map((e) => _PlaceItem(
account: widget.account,
place: e.place,
thumbUrl: NetworkRectThumbnail.imageUrlForFileId(
widget.account, e.latestFileId),
onTap: () => _onLocationTap(e),
))
.toList();
_countryItems = result.countryCode
.sorted(sorter)
.map((e) => _CountryItem(
account: widget.account,
country: e.place,
thumbUrl: NetworkRectThumbnail.imageUrlForFileId(
widget.account, e.latestFileId),
onTap: () => _onLocationTap(e),
))
.toList();
}
void _reqQuery() {
_bloc.add(ListLocationBlocQuery(widget.account));
}
late final _bloc = ListLocationBloc(KiwiContainer().resolve<DiContainer>());
var _placeItems = <_PlaceItem>[];
var _countryItems = <_CountryItem>[];
}
class _PlaceItem {
const _PlaceItem({
required this.account,
required this.place,
required this.thumbUrl,
this.onTap,
});
Widget buildWidget(BuildContext context) => CollectionListSmall(
label: place,
onTap: onTap,
child: _PlaceThumbnail(
account: account,
coverUrl: thumbUrl,
),
);
final Account account;
final String place;
final String thumbUrl;
final VoidCallback? onTap;
}
class _CountryItem {
const _CountryItem({
required this.account,
required this.country,
required this.thumbUrl,
this.onTap,
});
Widget buildWidget(BuildContext context) =>
_CountryItemView(thumbUrl, country, account: account, onTap: onTap);
final Account account;
final String country;
final String thumbUrl;
final VoidCallback? onTap;
}
class _CountryItemView extends StatelessWidget {
const _CountryItemView(
this.imageUrl,
this.text, {
required this.account,
this.onTap,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
NetworkRectThumbnail(
account: account,
imageUrl: imageUrl,
errorBuilder: (_) => const Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.location_on),
),
),
const SizedBox(width: 8),
Text(text),
const SizedBox(width: 8),
],
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: BorderRadius.circular(16),
),
),
),
if (onTap != null)
Positioned.fill(
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
),
),
),
],
),
);
}
final Account account;
final String imageUrl;
final String text;
final VoidCallback? onTap;
}
class _PlaceThumbnail extends StatelessWidget {
const _PlaceThumbnail({
required this.account,
required this.coverUrl,
});
@override
Widget build(BuildContext context) {
try {
return NetworkRectThumbnail(
account: account,
imageUrl: coverUrl!,
errorBuilder: (_) => const _Placeholder(),
);
} catch (_) {
return const FittedBox(
child: _Placeholder(),
);
}
}
final Account account;
final String? coverUrl;
}
class _Placeholder extends StatelessWidget {
const _Placeholder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.location_on,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
}
}