Custom radius (#1039)

This commit is contained in:
Ashil 2023-04-29 16:15:21 +05:30 committed by GitHub
commit 299d1bb117
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 342 additions and 122 deletions

View file

@ -60,8 +60,8 @@ const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];
const kilometersPerDegree = 111.16;
const radiusValues = <int>[1, 2, 10, 20, 40, 80, 200, 400, 1200];
const defaultRadiusValues = <double>[1, 2, 10, 20, 40, 80, 200, 400, 1200];
const defaultRadiusValueIndex = 4;
const defaultRadiusValue = 40.0;
const galleryGridSpacing = 2.0;

View file

@ -1399,7 +1399,8 @@ class FilesDB {
// user and upload time is greater than 20 April 2023 epoch time and less than
// 15 May 2023 epoch time
Future<List<String>> getFilesWithLocationUploadedBtw20AprTo15May2023(
int ownerID) async {
int ownerID,
) async {
final db = await database;
final result = await db.query(
filesTable,

View file

@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import "package:photos/core/constants.dart";
import 'package:photos/models/location/location.dart';
part 'location_tag.freezed.dart';
@ -10,7 +9,7 @@ class LocationTag with _$LocationTag {
const LocationTag._();
const factory LocationTag({
required String name,
required int radius,
required double radius,
required double aSquare,
required double bSquare,
required Location centerPoint,
@ -19,7 +18,7 @@ class LocationTag with _$LocationTag {
factory LocationTag.fromJson(Map<String, Object?> json) =>
_$LocationTagFromJson(json);
int get radiusIndex {
int radiusIndex(List<double> radiusValues) {
return radiusValues.indexOf(radius);
}
}

View file

@ -21,7 +21,7 @@ LocationTag _$LocationTagFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$LocationTag {
String get name => throw _privateConstructorUsedError;
int get radius => throw _privateConstructorUsedError;
double get radius => throw _privateConstructorUsedError;
double get aSquare => throw _privateConstructorUsedError;
double get bSquare => throw _privateConstructorUsedError;
Location get centerPoint => throw _privateConstructorUsedError;
@ -40,7 +40,7 @@ abstract class $LocationTagCopyWith<$Res> {
@useResult
$Res call(
{String name,
int radius,
double radius,
double aSquare,
double bSquare,
Location centerPoint});
@ -75,7 +75,7 @@ class _$LocationTagCopyWithImpl<$Res, $Val extends LocationTag>
radius: null == radius
? _value.radius
: radius // ignore: cast_nullable_to_non_nullable
as int,
as double,
aSquare: null == aSquare
? _value.aSquare
: aSquare // ignore: cast_nullable_to_non_nullable
@ -110,7 +110,7 @@ abstract class _$$_LocationTagCopyWith<$Res>
@useResult
$Res call(
{String name,
int radius,
double radius,
double aSquare,
double bSquare,
Location centerPoint});
@ -144,7 +144,7 @@ class __$$_LocationTagCopyWithImpl<$Res>
radius: null == radius
? _value.radius
: radius // ignore: cast_nullable_to_non_nullable
as int,
as double,
aSquare: null == aSquare
? _value.aSquare
: aSquare // ignore: cast_nullable_to_non_nullable
@ -178,7 +178,7 @@ class _$_LocationTag extends _LocationTag {
@override
final String name;
@override
final int radius;
final double radius;
@override
final double aSquare;
@override
@ -226,7 +226,7 @@ class _$_LocationTag extends _LocationTag {
abstract class _LocationTag extends LocationTag {
const factory _LocationTag(
{required final String name,
required final int radius,
required final double radius,
required final double aSquare,
required final double bSquare,
required final Location centerPoint}) = _$_LocationTag;
@ -238,7 +238,7 @@ abstract class _LocationTag extends LocationTag {
@override
String get name;
@override
int get radius;
double get radius;
@override
double get aSquare;
@override

View file

@ -9,7 +9,7 @@ part of 'location_tag.dart';
_$_LocationTag _$$_LocationTagFromJson(Map<String, dynamic> json) =>
_$_LocationTag(
name: json['name'] as String,
radius: json['radius'] as int,
radius: (json['radius'] as num).toDouble(),
aSquare: (json['aSquare'] as num).toDouble(),
bSquare: (json['bSquare'] as num).toDouble(),
centerPoint:

View file

@ -2,10 +2,14 @@ import 'dart:async';
import "package:photos/models/location/location.dart";
typedef FutureVoidCallback = Future<void> Function();
typedef BoolCallBack = bool Function();
typedef FutureVoidCallbackParamStr = Future<void> Function(String);
typedef VoidCallbackParamStr = void Function(String);
typedef FutureOrVoidCallback = FutureOr<void> Function();
typedef VoidCallbackParamInt = void Function(int);
typedef VoidCallbackParamDouble = Function(double);
typedef VoidCallbackParamListDouble = void Function(List<double>);
typedef VoidCallbackParamLocation = void Function(Location);
typedef FutureVoidCallback = Future<void> Function();
typedef FutureOrVoidCallback = FutureOr<void> Function();
typedef FutureVoidCallbackParamStr = Future<void> Function(String);

View file

@ -38,7 +38,7 @@ class LocationService {
Future<void> addLocation(
String location,
Location centerPoint,
int radius,
double radius,
) async {
//The area enclosed by the location tag will be a circle on a 3D spherical
//globe and an ellipse on a 2D Mercator projection (2D map)
@ -101,7 +101,7 @@ class LocationService {
bool isFileInsideLocationTag(
Location centerPoint,
Location fileCoordinates,
int radius,
double radius,
) {
final a =
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
@ -134,7 +134,7 @@ class LocationService {
///Will only update if there is a change in the locationTag's properties
Future<void> updateLocationTag({
required LocalEntity<LocationTag> locationTagEntity,
int? newRadius,
double? newRadius,
Location? newCenterPoint,
String? newName,
}) async {

View file

@ -15,7 +15,6 @@ import 'package:photos/models/search/album_search_result.dart';
import 'package:photos/models/search/generic_search_result.dart';
import 'package:photos/models/search/search_result.dart';
import 'package:photos/services/collections_service.dart';
import "package:photos/services/feature_flag_service.dart";
import "package:photos/services/location_service.dart";
import "package:photos/states/location_screen_state.dart";
import "package:photos/ui/viewer/location/location_screen.dart";

View file

@ -1,5 +1,6 @@
import "dart:async";
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/core/event_bus.dart";
@ -27,20 +28,29 @@ class LocationTagStateProvider extends StatefulWidget {
}
class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
int _selectedRaduisIndex = defaultRadiusValueIndex;
late double _selectedRadius;
late Location? _centerPoint;
late LocalEntity<LocationTag>? _locationTagEntity;
final Debouncer _selectedRadiusDebouncer =
Debouncer(const Duration(milliseconds: 300));
late final StreamSubscription _locTagEntityListener;
late final List<double> _radiusValues;
@override
void initState() {
_locationTagEntity = widget.locationTagEntity;
_centerPoint = widget.centerPoint;
assert(_centerPoint != null || _locationTagEntity != null);
_centerPoint = _locationTagEntity?.item.centerPoint ?? _centerPoint!;
_selectedRaduisIndex =
_locationTagEntity?.item.radiusIndex ?? defaultRadiusValueIndex;
///If the location tag has a custom radius value, we add the custom radius
///value to the list of default radius values only for this location tag and
///keep it in the state of this widget.
_radiusValues = _getRadiusValuesOfLocTag(_locationTagEntity?.item.radius);
_selectedRadius = _locationTagEntity?.item.radius ?? defaultRadiusValue;
_locTagEntityListener =
Bus.instance.on<LocationTagUpdatedEvent>().listen((event) {
_locationTagUpdateListener(event);
@ -57,10 +67,11 @@ class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
void _locationTagUpdateListener(LocationTagUpdatedEvent event) {
if (event.type == LocTagEventType.update) {
if (event.updatedLocTagEntities!.first.id == _locationTagEntity!.id) {
//Update state when locationTag is updated.
setState(() {
final updatedLocTagEntity = event.updatedLocTagEntities!.first;
_selectedRaduisIndex = updatedLocTagEntity.item.radiusIndex;
_selectedRadius = updatedLocTagEntity.item.radius;
_centerPoint = updatedLocTagEntity.item.centerPoint;
_locationTagEntity = updatedLocTagEntity;
});
@ -68,12 +79,12 @@ class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
}
}
void _updateSelectedIndex(int index) {
void _updateSelectedRadius(double radius) {
_selectedRadiusDebouncer.cancelDebounce();
_selectedRadiusDebouncer.run(() async {
if (mounted) {
setState(() {
_selectedRaduisIndex = index;
_selectedRadius = radius;
});
}
});
@ -87,14 +98,42 @@ class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
}
}
void _updateRadiusValues(List<double> radiusValues) {
if (mounted) {
setState(() {
for (double radiusValue in radiusValues) {
if (!_radiusValues.contains(radiusValue)) {
_radiusValues.add(radiusValue);
}
}
_radiusValues.sort();
});
}
}
///Returns the list of radius values for the location tag entity. If radius of
///the location tag is not present in the default list, it returns the list
///with the custom radius value.
List<double> _getRadiusValuesOfLocTag(double? radiusOfLocTag) {
final radiusValues = <double>[...defaultRadiusValues];
if (radiusOfLocTag != null &&
!defaultRadiusValues.contains(radiusOfLocTag)) {
radiusValues.add(radiusOfLocTag);
radiusValues.sort();
}
return radiusValues;
}
@override
Widget build(BuildContext context) {
return InheritedLocationTagData(
_selectedRaduisIndex,
_selectedRadius,
_centerPoint!,
_updateSelectedIndex,
_updateSelectedRadius,
_locationTagEntity,
_updateCenterPoint,
_updateRadiusValues,
_radiusValues,
child: widget.child,
);
}
@ -102,18 +141,22 @@ class _LocationTagStateProviderState extends State<LocationTagStateProvider> {
///This InheritedWidget's state is used in add & edit location sheets
class InheritedLocationTagData extends InheritedWidget {
final int selectedRadiusIndex;
final double selectedRadius;
final Location centerPoint;
//locationTag is null when we are creating a new location tag in add location sheet
final LocalEntity<LocationTag>? locationTagEntity;
final VoidCallbackParamInt updateSelectedIndex;
final VoidCallbackParamDouble updateSelectedRadius;
final VoidCallbackParamLocation updateCenterPoint;
final VoidCallbackParamListDouble updateRadiusValues;
final List<double> radiusValues;
const InheritedLocationTagData(
this.selectedRadiusIndex,
this.selectedRadius,
this.centerPoint,
this.updateSelectedIndex,
this.updateSelectedRadius,
this.locationTagEntity,
this.updateCenterPoint, {
this.updateCenterPoint,
this.updateRadiusValues,
this.radiusValues, {
required super.child,
super.key,
});
@ -125,7 +168,10 @@ class InheritedLocationTagData extends InheritedWidget {
@override
bool updateShouldNotify(InheritedLocationTagData oldWidget) {
return oldWidget.selectedRadiusIndex != selectedRadiusIndex ||
print(selectedRadius);
print(oldWidget.selectedRadius != selectedRadius);
return oldWidget.selectedRadius != selectedRadius ||
!oldWidget.radiusValues.equals(radiusValues) ||
oldWidget.centerPoint != centerPoint ||
oldWidget.locationTagEntity != locationTagEntity;
}

View file

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import "package:flutter/services.dart";
import 'package:photos/core/constants.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/models/search/button_result.dart";
@ -174,6 +175,9 @@ class TextInputDialog extends StatefulWidget {
final TextCapitalization? textCapitalization;
final bool alwaysShowSuccessState;
final bool isPasswordInput;
final TextEditingController? textEditingController;
final List<TextInputFormatter>? textInputFormatter;
final TextInputType? textInputType;
const TextInputDialog({
required this.title,
this.body,
@ -191,6 +195,9 @@ class TextInputDialog extends StatefulWidget {
this.showOnlyLoadingState = false,
this.alwaysShowSuccessState = false,
this.isPasswordInput = false,
this.textEditingController,
this.textInputFormatter,
this.textInputType,
super.key,
});
@ -201,10 +208,28 @@ class TextInputDialog extends StatefulWidget {
class _TextInputDialogState extends State<TextInputDialog> {
//the value of this ValueNotifier has no significance
final _submitNotifier = ValueNotifier(false);
late final ValueNotifier<bool> _inputIsEmptyNotifier;
late final TextEditingController _textEditingController;
@override
void initState() {
_textEditingController =
widget.textEditingController ?? TextEditingController();
_inputIsEmptyNotifier = widget.initialValue?.isEmpty ?? true
? ValueNotifier(true)
: ValueNotifier(false);
_textEditingController.addListener(() {
if (_textEditingController.text.isEmpty != _inputIsEmptyNotifier.value) {
_inputIsEmptyNotifier.value = _textEditingController.text.isEmpty;
}
});
super.initState();
}
@override
void dispose() {
_submitNotifier.dispose();
_inputIsEmptyNotifier.dispose();
super.dispose();
}
@ -251,6 +276,9 @@ class _TextInputDialogState extends State<TextInputDialog> {
textCapitalization: widget.textCapitalization,
alwaysShowSuccessState: widget.alwaysShowSuccessState,
isPasswordInput: widget.isPasswordInput,
textEditingController: _textEditingController,
textInputFormatter: widget.textInputFormatter,
textInputType: widget.textInputType,
),
),
const SizedBox(height: 36),
@ -267,12 +295,18 @@ class _TextInputDialogState extends State<TextInputDialog> {
),
const SizedBox(width: 8),
Expanded(
child: ButtonWidget(
buttonSize: ButtonSize.small,
buttonType: ButtonType.neutral,
labelText: widget.submitButtonLabel,
onTap: () async {
_submitNotifier.value = !_submitNotifier.value;
child: ValueListenableBuilder(
valueListenable: _inputIsEmptyNotifier,
builder: (context, bool value, _) {
return ButtonWidget(
buttonSize: ButtonSize.small,
buttonType: ButtonType.neutral,
labelText: widget.submitButtonLabel,
isDisabled: value,
onTap: () async {
_submitNotifier.value = !_submitNotifier.value;
},
);
},
),
),

View file

@ -41,6 +41,8 @@ class TextInputWidget extends StatefulWidget {
final VoidCallback? onCancel;
final TextEditingController? textEditingController;
final ValueNotifier? isEmptyNotifier;
final List<TextInputFormatter>? textInputFormatter;
final TextInputType? textInputType;
const TextInputWidget({
this.onSubmit,
this.onChange,
@ -67,6 +69,8 @@ class TextInputWidget extends StatefulWidget {
this.onCancel,
this.textEditingController,
this.isEmptyNotifier,
this.textInputFormatter,
this.textInputType,
super.key,
});
@ -90,12 +94,8 @@ class _TextInputWidgetState extends State<TextInputWidget> {
widget.cancelNotifier?.addListener(_onCancel);
_textController = widget.textEditingController ?? TextEditingController();
if (widget.initialValue != null) {
_textController.value = TextEditingValue(
text: widget.initialValue!,
selection: TextSelection.collapsed(offset: widget.initialValue!.length),
);
}
_setInitialValue();
if (widget.onChange != null) {
_textController.addListener(() {
widget.onChange!.call(_textController.text);
@ -143,13 +143,15 @@ class _TextInputWidgetState extends State<TextInputWidget> {
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius)),
child: Material(
child: TextFormField(
keyboardType: widget.textInputType,
textCapitalization: widget.textCapitalization!,
autofocus: widget.autoFocus ?? false,
controller: _textController,
focusNode: widget.focusNode,
inputFormatters: widget.maxLength != null
? [LengthLimitingTextInputFormatter(50)]
: null,
inputFormatters: widget.textInputFormatter ??
(widget.maxLength != null
? [LengthLimitingTextInputFormatter(50)]
: null),
obscureText: _obscureTextNotifier.value,
decoration: InputDecoration(
hintText: widget.hintText,
@ -343,6 +345,42 @@ class _TextInputWidgetState extends State<TextInputWidget> {
void _popNavigatorStack(BuildContext context, {Exception? e}) {
Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null;
}
void _setInitialValue() {
if (widget.initialValue != null) {
final formattedInitialValue = _formatInitialValue(
widget.initialValue!,
widget.textInputFormatter,
);
_textController.value = TextEditingValue(
text: formattedInitialValue,
selection:
TextSelection.collapsed(offset: formattedInitialValue.length),
);
}
}
String _formatInitialValue(
String initialValue,
List<TextInputFormatter>? formatters,
) {
if (formatters == null || formatters.isEmpty) {
return initialValue;
}
String formattedValue = initialValue;
for (final formatter in formatters) {
formattedValue = formatter
.formatEditUpdate(
TextEditingValue.empty,
TextEditingValue(text: formattedValue),
)
.text;
}
return formattedValue;
}
}
//todo: Add clear and custom icon for suffic icon

View file

@ -51,14 +51,17 @@ class AddLocationSheet extends StatefulWidget {
}
class _AddLocationSheetState extends State<AddLocationSheet> {
//The value of these notifiers has no significance.
//The value of this notifier has no significance.
//When memoriesCountNotifier is null, we show the loading widget in the
//memories count section which also means the gallery is loading.
final ValueNotifier<int?> _memoriesCountNotifier = ValueNotifier(null);
//The value of this notifier has no significance.
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
final ValueNotifier<int> _selectedRadiusIndexNotifier =
ValueNotifier(defaultRadiusValueIndex);
final ValueNotifier<double> _selectedRadiusNotifier =
ValueNotifier(defaultRadiusValue);
final _focusNode = FocusNode();
final _textEditingController = TextEditingController();
final _isEmptyNotifier = ValueNotifier(true);
@ -67,7 +70,7 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
@override
void initState() {
_focusNode.addListener(_focusNodeListener);
_selectedRadiusIndexNotifier.addListener(_selectedRadiusIndexListener);
_selectedRadiusNotifier.addListener(_selectedRadiusListener);
super.initState();
}
@ -76,7 +79,7 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
_focusNode.removeListener(_focusNodeListener);
_submitNotifer.dispose();
_cancelNotifier.dispose();
_selectedRadiusIndexNotifier.dispose();
_selectedRadiusNotifier.dispose();
super.dispose();
}
@ -149,9 +152,9 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
),
const SizedBox(height: 24),
RadiusPickerWidget(
_selectedRadiusIndexNotifier,
_selectedRadiusNotifier,
),
const SizedBox(height: 20),
const SizedBox(height: 16),
Text(
S.of(context).locationTagFeatureDescription,
style: textTheme.smallMuted,
@ -230,7 +233,8 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
Future<void> _addLocationTag() async {
final locationData = InheritedLocationTagData.of(context);
final coordinates = locationData.centerPoint;
final radius = radiusValues[locationData.selectedRadiusIndex];
final radius = locationData.selectedRadius;
await LocationService.instance.addLocation(
_textEditingController.text.trim(),
coordinates,
@ -256,11 +260,11 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
}
}
void _selectedRadiusIndexListener() {
void _selectedRadiusListener() {
InheritedLocationTagData.of(
context,
).updateSelectedIndex(
_selectedRadiusIndexNotifier.value,
).updateSelectedRadius(
_selectedRadiusNotifier.value,
);
_memoriesCountNotifier.value = null;
}

View file

@ -55,7 +55,7 @@ class _DynamicLocationGalleryWidgetState
@override
Widget build(BuildContext context) {
const galleryFilesLimit = 1000;
final selectedRadius = _selectedRadius();
final selectedRadius = InheritedLocationTagData.of(context).selectedRadius;
Future<FileLoadResult> filterFiles() async {
final FileLoadResult result = await fileLoadResult;
//wait for ignored files to be removed after init
@ -121,11 +121,6 @@ class _DynamicLocationGalleryWidgetState
);
}
int _selectedRadius() {
return radiusValues[
InheritedLocationTagData.of(context).selectedRadiusIndex];
}
double _galleryHeight(int fileCount) {
final photoGridSize = LocalSettings.instance.getPhotoGridSize();
final totalWhiteSpaceBetweenPhotos =

View file

@ -17,8 +17,8 @@ class EditCenterPointTileWidget extends StatelessWidget {
return Row(
children: [
Container(
width: 48,
height: 48,
width: 52,
height: 52,
color: colorScheme.fillFaint,
child: Icon(
Icons.location_on_outlined,

View file

@ -61,8 +61,8 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
final ValueNotifier<int?> _memoriesCountNotifier = ValueNotifier(null);
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
final ValueNotifier<int> _selectedRadiusIndexNotifier =
ValueNotifier(defaultRadiusValueIndex);
final ValueNotifier<double> _selectedRadiusNotifier =
ValueNotifier(defaultRadiusValue);
final _focusNode = FocusNode();
final _textEditingController = TextEditingController();
final _isEmptyNotifier = ValueNotifier(false);
@ -71,7 +71,7 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
@override
void initState() {
_focusNode.addListener(_focusNodeListener);
_selectedRadiusIndexNotifier.addListener(_selectedRadiusIndexListener);
_selectedRadiusNotifier.addListener(_selectedRadiusListener);
super.initState();
}
@ -80,7 +80,7 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
_focusNode.removeListener(_focusNodeListener);
_submitNotifer.dispose();
_cancelNotifier.dispose();
_selectedRadiusIndexNotifier.dispose();
_selectedRadiusNotifier.dispose();
super.dispose();
}
@ -162,9 +162,9 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
const EditCenterPointTileWidget(),
const SizedBox(height: 20),
RadiusPickerWidget(
_selectedRadiusIndexNotifier,
_selectedRadiusNotifier,
),
const SizedBox(height: 20),
const SizedBox(height: 16),
],
),
),
@ -240,7 +240,7 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
final locationTagState = InheritedLocationTagData.of(context);
await LocationService.instance.updateLocationTag(
locationTagEntity: locationTagState.locationTagEntity!,
newRadius: radiusValues[locationTagState.selectedRadiusIndex],
newRadius: locationTagState.selectedRadius,
newName: _textEditingController.text.trim(),
newCenterPoint: InheritedLocationTagData.of(context).centerPoint,
);
@ -264,11 +264,11 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
}
}
void _selectedRadiusIndexListener() {
void _selectedRadiusListener() {
InheritedLocationTagData.of(
context,
).updateSelectedIndex(
_selectedRadiusIndexNotifier.value,
).updateSelectedRadius(
_selectedRadiusNotifier.value,
);
_memoriesCountNotifier.value = null;
}

View file

@ -1,9 +1,11 @@
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:flutter/services.dart";
import "package:logging/logging.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/states/location_state.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/utils/dialog_util.dart";
class CustomTrackShape extends RoundedRectSliderTrackShape {
@override
@ -21,11 +23,11 @@ class CustomTrackShape extends RoundedRectSliderTrackShape {
}
class RadiusPickerWidget extends StatefulWidget {
///This notifier can be listened to get the selected radius index from
///a parent widget.
final ValueNotifier<int> selectedRadiusIndexNotifier;
///This notifier can be listened from a parent widget to get the selected radius
final ValueNotifier<double> selectedRadiusNotifier;
const RadiusPickerWidget(
this.selectedRadiusIndexNotifier, {
this.selectedRadiusNotifier, {
super.key,
});
@ -34,6 +36,7 @@ class RadiusPickerWidget extends StatefulWidget {
}
class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
final _logger = Logger("RadiusPickerWidget");
@override
void initState() {
super.initState();
@ -41,51 +44,62 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
@override
void didChangeDependencies() {
widget.selectedRadiusIndexNotifier.value =
InheritedLocationTagData.of(context).selectedRadiusIndex;
widget.selectedRadiusNotifier.value =
InheritedLocationTagData.of(context).selectedRadius;
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
final selectedRadiusIndex = widget.selectedRadiusIndexNotifier.value;
final radiusValue = radiusValues[selectedRadiusIndex];
final radiusValues = InheritedLocationTagData.of(context).radiusValues;
final selectedRadius = widget.selectedRadiusNotifier.value;
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
final roundedRadius = roundRadius(selectedRadius);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: colorScheme.fillFaint,
borderRadius: const BorderRadius.all(Radius.circular(2)),
),
padding: const EdgeInsets.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 6,
child: Text(
radiusValue.toString(),
style: radiusValue != 1200
? textTheme.largeBold
: textTheme.bodyBold,
textAlign: TextAlign.center,
),
GestureDetector(
onTap: _customRadiusOnTap,
child: Container(
height: 52,
width: 52,
decoration: BoxDecoration(
color: colorScheme.fillFaint,
borderRadius: const BorderRadius.all(Radius.circular(2)),
border: Border.all(
color: colorScheme.strokeFainter,
width: 1,
strokeAlign: BorderSide.strokeAlignInside,
),
Expanded(
flex: 5,
child: Text(
S.of(context).kiloMeterUnit,
style: textTheme.miniMuted,
),
padding: const EdgeInsets.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 6,
child: Center(
child: Text(
roundedRadius,
style: double.parse(roundedRadius) < 1000
? textTheme.largeBold
: textTheme.bodyBold,
textAlign: TextAlign.center,
),
),
),
),
],
Expanded(
flex: 5,
child: Text(
S.of(context).kiloMeterUnit,
style: textTheme.miniMuted,
),
),
],
),
),
),
const SizedBox(width: 4),
@ -96,6 +110,7 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
Text(S.of(context).radius, style: textTheme.body),
const SizedBox(height: 16),
SizedBox(
@ -120,11 +135,11 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
),
child: RepaintBoundary(
child: Slider(
value: selectedRadiusIndex.toDouble(),
value: radiusValues.indexOf(selectedRadius).toDouble(),
onChanged: (value) {
setState(() {
widget.selectedRadiusIndexNotifier.value =
value.toInt();
widget.selectedRadiusNotifier.value =
radiusValues[value.toInt()];
});
},
min: 0,
@ -141,4 +156,82 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
],
);
}
//9.99 -> 10, 9.0 -> 9, 5.02 -> 5, 5.09 -> 5.1
//12.3 -> 12, 121.65 -> 122, 999.9 -> 1000
String roundRadius(double radius) {
String result;
final roundedRadius = (radius * 10).round() / 10;
if (radius >= 10) {
result = roundedRadius.toStringAsFixed(0);
} else {
if (roundedRadius == roundedRadius.truncate()) {
result = roundedRadius.truncate().toString();
} else {
result = roundedRadius.toStringAsFixed(1);
}
}
return result;
}
Future<void> _customRadiusOnTap() async {
final result = await showTextInputDialog(
context,
title: "Custom radius",
onSubmit: (customRadius) async {
final radius = double.tryParse(customRadius);
if (radius != null) {
final locationTagState = InheritedLocationTagData.of(context);
locationTagState.updateRadiusValues([radius]);
if (mounted) {
setState(() {
widget.selectedRadiusNotifier.value = radius;
});
}
} else {
throw Exception("Radius is null");
}
},
submitButtonLabel: "Done",
textInputFormatter: [NumberWithDecimalInputFormatter(maxValue: 10000)],
textInputType: const TextInputType.numberWithOptions(decimal: true),
message: "km",
alignMessage: Alignment.centerRight,
);
if (result is Exception) {
await showGenericErrorDialog(context: context);
_logger.severe(
"Failed to create custom radius",
result,
);
}
}
}
class NumberWithDecimalInputFormatter extends TextInputFormatter {
final RegExp _pattern = RegExp(r'^(?:\d+(\.\d*)?)?$');
final double maxValue;
NumberWithDecimalInputFormatter({required this.maxValue});
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Check if the new value matches the pattern
if (_pattern.hasMatch(newValue.text)) {
if (newValue.text.isEmpty) {
return newValue;
}
final newValueAsDouble = double.tryParse(newValue.text);
// Check if the new value is within the allowed range
if (newValueAsDouble != null && newValueAsDouble <= maxValue) {
return newValue;
}
}
return oldValue;
}
}

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:confetti/confetti.dart';
import "package:dio/dio.dart";
import 'package:flutter/material.dart';
import "package:flutter/services.dart";
import 'package:photos/core/constants.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/models/search/button_result.dart";
@ -303,6 +304,9 @@ Future<dynamic> showTextInputDialog(
TextCapitalization textCapitalization = TextCapitalization.none,
bool alwaysShowSuccessState = false,
bool isPasswordInput = false,
TextEditingController? textEditingController,
List<TextInputFormatter>? textInputFormatter,
TextInputType? textInputType,
}) {
return showDialog(
barrierColor: backdropFaintDark,
@ -330,6 +334,9 @@ Future<dynamic> showTextInputDialog(
textCapitalization: textCapitalization,
alwaysShowSuccessState: alwaysShowSuccessState,
isPasswordInput: isPasswordInput,
textEditingController: textEditingController,
textInputFormatter: textInputFormatter,
textInputType: textInputType,
),
),
);