feat(mobile): map view (#3661)

* feat(mobile): map page - add map view

* map: add map-markers

* feat(map): add relative date filter

* fix: do not let users scroll past map bounds

* fix: fetch relative date from store to state on init

* feat(mobile):re-fetch markers only on filter change

* feat(mobile) - asset bottom sheet in map page

* feat(mobile): display markers based on bottom sheet scroll

* fix: exif-bottom-sheet - rebase conflict

* feat(mobile): map-view - strongly typed map page events

* feat(map): zoom to asset

* chore: dart analyzer fixes

* map-page move attribution to top-right

* feat(mobile): map view - asset selection handling

* feat(mobile): map-view display map in places row

* fix: make asset marker icon responsive

* optimise map page rebuilds

* refactor(mobile): map page

* feat(mobile): map-view: Go to location

* map-view(mobile): minor refactor

* fix(mobile): Handle invalid coords gracefully

* small styling

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
shalong-tanwen
2023-08-27 05:07:35 +00:00
committed by GitHub
parent 305889f32b
commit cb391342d7
37 changed files with 2268 additions and 139 deletions

View File

@@ -0,0 +1,104 @@
import 'package:flutter/widgets.dart';
class InvertionFilter extends StatelessWidget {
final Widget? child;
const InvertionFilter({super.key, this.child});
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255, //
0, -1, 0, 0, 255, //
0, 0, -1, 0, 255, //
0, 0, 0, 1, 0, //
]),
child: child,
);
}
}
// -1 - darkest, 1 - brightest, 0 - unchanged
class BrightnessFilter extends StatelessWidget {
final Widget? child;
final double brightness;
const BrightnessFilter({super.key, this.child, this.brightness = 0});
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.matrix(
_ColorFilterGenerator.brightnessAdjustMatrix(brightness),
),
child: child,
);
}
}
// -1 - greyscale, 1 - most saturated, 0 - unchanged
class SaturationFilter extends StatelessWidget {
final Widget? child;
final double saturation;
const SaturationFilter({super.key, this.child, this.saturation = 0});
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.matrix(
_ColorFilterGenerator.saturationAdjustMatrix(saturation),
),
child: child,
);
}
}
class _ColorFilterGenerator {
static List<double> brightnessAdjustMatrix(double value) {
value = value * 10;
if (value == 0) {
return [
1, 0, 0, 0, 0, //
0, 1, 0, 0, 0, //
0, 0, 1, 0, 0, //
0, 0, 0, 1, 0, //
];
}
return List<double>.from(<double>[
1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, //
]).map((i) => i.toDouble()).toList();
}
static List<double> saturationAdjustMatrix(double value) {
value = value * 100;
if (value == 0) {
return [
1, 0, 0, 0, 0, //
0, 1, 0, 0, 0, //
0, 0, 1, 0, 0, //
0, 0, 0, 1, 0, //
];
}
double x =
((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble();
double lumR = 0.3086;
double lumG = 0.6094;
double lumB = 0.082;
return List<double>.from(<double>[
(lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x), //
0, 0, //
lumR * (1 - x), //
(lumG * (1 - x)) + x, //
lumB * (1 - x), //
0, 0, //
lumR * (1 - x), //
lumG * (1 - x), //
(lumB * (1 - x)) + x, //
0, 0, 0, 0, 0, 1, 0, //
]).map((i) => i.toDouble()).toList();
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:async';
import 'package:flutter/material.dart';
class Debounce {
Debounce(Duration interval) : _interval = interval.inMilliseconds;
final int _interval;
Timer? _timer;
VoidCallback? action;
void call(VoidCallback? action) {
this.action = action;
_timer?.cancel();
_timer = Timer(Duration(milliseconds: _interval), _callAndRest);
}
void _callAndRest() {
action?.call();
_timer = null;
}
void dispose() {
_timer?.cancel();
_timer = null;
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'dart:math' as math;
extension MoveByBounds on MapController {
// TODO: Remove this in favor of built-in method when upgrading flutter_map to 5.0.0
LatLng? centerBoundsWithPadding(
LatLng coordinates,
Offset offset, {
double? zoomLevel,
}) {
const crs = Epsg3857();
final oldCenterPt = crs.latLngToPoint(coordinates, zoomLevel ?? zoom);
final mapCenterPoint = _rotatePoint(
oldCenterPt,
oldCenterPt - CustomPoint(offset.dx, offset.dy),
);
return crs.pointToLatLng(mapCenterPoint, zoomLevel ?? zoom);
}
CustomPoint<double> _rotatePoint(
CustomPoint<double> mapCenter,
CustomPoint<double> point, {
bool counterRotation = true,
}) {
final counterRotationFactor = counterRotation ? -1 : 1;
final m = Matrix4.identity()
..translate(mapCenter.x, mapCenter.y)
..rotateZ(degToRadian(rotation) * counterRotationFactor)
..translate(-mapCenter.x, -mapCenter.y);
final tp = MatrixUtils.transformPoint(m, Offset(point.x, point.y));
return CustomPoint(tp.dx, tp.dy);
}
double getTapThresholdForZoomLevel() {
const scale = [
25000000,
15000000,
8000000,
4000000,
2000000,
1000000,
500000,
250000,
100000,
50000,
25000,
15000,
8000,
4000,
2000,
1000,
500,
250,
100,
50,
25,
10,
5,
];
return scale[math.max(0, math.min(20, zoom.round() + 2))].toDouble() / 6;
}
}

View File

@@ -7,17 +7,20 @@ String getThumbnailUrl(
final Asset asset, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
return _getThumbnailUrl(asset.remoteId!, type: type);
return getThumbnailUrlForRemoteId(asset.remoteId!, type: type);
}
String getThumbnailCacheKey(
final Asset asset, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
return _getThumbnailCacheKey(asset.remoteId!, type);
return getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type);
}
String _getThumbnailCacheKey(final String id, final ThumbnailFormat type) {
String getThumbnailCacheKeyForRemoteId(
final String id, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
if (type == ThumbnailFormat.WEBP) {
return 'thumbnail-image-$id';
} else {
@@ -32,7 +35,8 @@ String getAlbumThumbnailUrl(
if (album.thumbnail.value?.remoteId == null) {
return '';
}
return _getThumbnailUrl(album.thumbnail.value!.remoteId!, type: type);
return getThumbnailUrlForRemoteId(album.thumbnail.value!.remoteId!,
type: type,);
}
String getAlbumThumbNailCacheKey(
@@ -42,7 +46,10 @@ String getAlbumThumbNailCacheKey(
if (album.thumbnail.value?.remoteId == null) {
return '';
}
return _getThumbnailCacheKey(album.thumbnail.value!.remoteId!, type);
return getThumbnailCacheKeyForRemoteId(
album.thumbnail.value!.remoteId!,
type: type,
);
}
String getImageUrl(final Asset asset) {
@@ -53,7 +60,7 @@ String getImageCacheKey(final Asset asset) {
return '${asset.id}_fullStage';
}
String _getThumbnailUrl(
String getThumbnailUrlForRemoteId(
final String id, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
void handleShareAssets(
WidgetRef ref,
BuildContext context,
List<Asset> selection,
) {
showDialog(
context: context,
builder: (BuildContext buildContext) {
ref
.watch(shareServiceProvider)
.shareAssets(selection.toList())
.then((_) => Navigator.of(buildContext).pop());
return const ShareDialog();
},
barrierDismissible: false,
);
}
Future<void> handleArchiveAssets(
WidgetRef ref,
BuildContext context,
List<Asset> selection, {
bool shouldArchive = true,
ToastGravity toastGravity = ToastGravity.BOTTOM,
}) async {
if (selection.isNotEmpty) {
await ref
.read(assetProvider.notifier)
.toggleArchive(selection, shouldArchive);
final assetOrAssets = selection.length > 1 ? 'assets' : 'asset';
final archiveOrLibrary = shouldArchive ? 'archive' : 'library';
if (context.mounted) {
ImmichToast.show(
context: context,
msg: 'Moved ${selection.length} $assetOrAssets to $archiveOrLibrary',
gravity: toastGravity,
);
}
}
}
Future<void> handleFavoriteAssets(
WidgetRef ref,
BuildContext context,
List<Asset> selection, {
bool shouldFavorite = true,
ToastGravity toastGravity = ToastGravity.BOTTOM,
}) async {
if (selection.isNotEmpty) {
await ref
.watch(assetProvider.notifier)
.toggleFavorite(selection, shouldFavorite);
final assetOrAssets = selection.length > 1 ? 'assets' : 'asset';
final toastMessage = shouldFavorite
? 'Added ${selection.length} $assetOrAssets to favorites'
: 'Removed ${selection.length} $assetOrAssets from favorites';
if (context.mounted) {
ImmichToast.show(
context: context,
msg: toastMessage,
gravity: ToastGravity.BOTTOM,
);
}
}
}