mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-23 01:06:21 +01:00
339 lines
9.4 KiB
Dart
339 lines
9.4 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(
|
|
account: account,
|
|
label: place,
|
|
coverUrl: thumbUrl,
|
|
fallbackBuilder: (context) => Icon(
|
|
Icons.location_on,
|
|
color: Theme.of(context).listPlaceholderForegroundColor,
|
|
),
|
|
onTap: onTap,
|
|
);
|
|
|
|
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;
|
|
}
|