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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,14 @@ import 'dart:async';
import "package:photos/models/location/location.dart"; import "package:photos/models/location/location.dart";
typedef FutureVoidCallback = Future<void> Function();
typedef BoolCallBack = bool Function(); typedef BoolCallBack = bool Function();
typedef FutureVoidCallbackParamStr = Future<void> Function(String);
typedef VoidCallbackParamStr = void Function(String); typedef VoidCallbackParamStr = void Function(String);
typedef FutureOrVoidCallback = FutureOr<void> Function();
typedef VoidCallbackParamInt = void Function(int); typedef VoidCallbackParamInt = void Function(int);
typedef VoidCallbackParamDouble = Function(double);
typedef VoidCallbackParamListDouble = void Function(List<double>);
typedef VoidCallbackParamLocation = void Function(Location); 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( Future<void> addLocation(
String location, String location,
Location centerPoint, Location centerPoint,
int radius, double radius,
) async { ) async {
//The area enclosed by the location tag will be a circle on a 3D spherical //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) //globe and an ellipse on a 2D Mercator projection (2D map)
@ -101,7 +101,7 @@ class LocationService {
bool isFileInsideLocationTag( bool isFileInsideLocationTag(
Location centerPoint, Location centerPoint,
Location fileCoordinates, Location fileCoordinates,
int radius, double radius,
) { ) {
final a = final a =
(radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree; (radius * _scaleFactor(centerPoint.latitude!)) / kilometersPerDegree;
@ -134,7 +134,7 @@ class LocationService {
///Will only update if there is a change in the locationTag's properties ///Will only update if there is a change in the locationTag's properties
Future<void> updateLocationTag({ Future<void> updateLocationTag({
required LocalEntity<LocationTag> locationTagEntity, required LocalEntity<LocationTag> locationTagEntity,
int? newRadius, double? newRadius,
Location? newCenterPoint, Location? newCenterPoint,
String? newName, String? newName,
}) async { }) 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/generic_search_result.dart';
import 'package:photos/models/search/search_result.dart'; import 'package:photos/models/search/search_result.dart';
import 'package:photos/services/collections_service.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/services/location_service.dart";
import "package:photos/states/location_screen_state.dart"; import "package:photos/states/location_screen_state.dart";
import "package:photos/ui/viewer/location/location_screen.dart"; import "package:photos/ui/viewer/location/location_screen.dart";

View file

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

View file

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import "package:flutter/services.dart";
import 'package:photos/core/constants.dart'; import 'package:photos/core/constants.dart';
import "package:photos/generated/l10n.dart"; import "package:photos/generated/l10n.dart";
import "package:photos/models/search/button_result.dart"; import "package:photos/models/search/button_result.dart";
@ -174,6 +175,9 @@ class TextInputDialog extends StatefulWidget {
final TextCapitalization? textCapitalization; final TextCapitalization? textCapitalization;
final bool alwaysShowSuccessState; final bool alwaysShowSuccessState;
final bool isPasswordInput; final bool isPasswordInput;
final TextEditingController? textEditingController;
final List<TextInputFormatter>? textInputFormatter;
final TextInputType? textInputType;
const TextInputDialog({ const TextInputDialog({
required this.title, required this.title,
this.body, this.body,
@ -191,6 +195,9 @@ class TextInputDialog extends StatefulWidget {
this.showOnlyLoadingState = false, this.showOnlyLoadingState = false,
this.alwaysShowSuccessState = false, this.alwaysShowSuccessState = false,
this.isPasswordInput = false, this.isPasswordInput = false,
this.textEditingController,
this.textInputFormatter,
this.textInputType,
super.key, super.key,
}); });
@ -201,10 +208,28 @@ class TextInputDialog extends StatefulWidget {
class _TextInputDialogState extends State<TextInputDialog> { class _TextInputDialogState extends State<TextInputDialog> {
//the value of this ValueNotifier has no significance //the value of this ValueNotifier has no significance
final _submitNotifier = ValueNotifier(false); 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 @override
void dispose() { void dispose() {
_submitNotifier.dispose(); _submitNotifier.dispose();
_inputIsEmptyNotifier.dispose();
super.dispose(); super.dispose();
} }
@ -251,6 +276,9 @@ class _TextInputDialogState extends State<TextInputDialog> {
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
alwaysShowSuccessState: widget.alwaysShowSuccessState, alwaysShowSuccessState: widget.alwaysShowSuccessState,
isPasswordInput: widget.isPasswordInput, isPasswordInput: widget.isPasswordInput,
textEditingController: _textEditingController,
textInputFormatter: widget.textInputFormatter,
textInputType: widget.textInputType,
), ),
), ),
const SizedBox(height: 36), const SizedBox(height: 36),
@ -267,12 +295,18 @@ class _TextInputDialogState extends State<TextInputDialog> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: ButtonWidget( child: ValueListenableBuilder(
buttonSize: ButtonSize.small, valueListenable: _inputIsEmptyNotifier,
buttonType: ButtonType.neutral, builder: (context, bool value, _) {
labelText: widget.submitButtonLabel, return ButtonWidget(
onTap: () async { buttonSize: ButtonSize.small,
_submitNotifier.value = !_submitNotifier.value; 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 VoidCallback? onCancel;
final TextEditingController? textEditingController; final TextEditingController? textEditingController;
final ValueNotifier? isEmptyNotifier; final ValueNotifier? isEmptyNotifier;
final List<TextInputFormatter>? textInputFormatter;
final TextInputType? textInputType;
const TextInputWidget({ const TextInputWidget({
this.onSubmit, this.onSubmit,
this.onChange, this.onChange,
@ -67,6 +69,8 @@ class TextInputWidget extends StatefulWidget {
this.onCancel, this.onCancel,
this.textEditingController, this.textEditingController,
this.isEmptyNotifier, this.isEmptyNotifier,
this.textInputFormatter,
this.textInputType,
super.key, super.key,
}); });
@ -90,12 +94,8 @@ class _TextInputWidgetState extends State<TextInputWidget> {
widget.cancelNotifier?.addListener(_onCancel); widget.cancelNotifier?.addListener(_onCancel);
_textController = widget.textEditingController ?? TextEditingController(); _textController = widget.textEditingController ?? TextEditingController();
if (widget.initialValue != null) { _setInitialValue();
_textController.value = TextEditingValue(
text: widget.initialValue!,
selection: TextSelection.collapsed(offset: widget.initialValue!.length),
);
}
if (widget.onChange != null) { if (widget.onChange != null) {
_textController.addListener(() { _textController.addListener(() {
widget.onChange!.call(_textController.text); widget.onChange!.call(_textController.text);
@ -143,13 +143,15 @@ class _TextInputWidgetState extends State<TextInputWidget> {
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius)), borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius)),
child: Material( child: Material(
child: TextFormField( child: TextFormField(
keyboardType: widget.textInputType,
textCapitalization: widget.textCapitalization!, textCapitalization: widget.textCapitalization!,
autofocus: widget.autoFocus ?? false, autofocus: widget.autoFocus ?? false,
controller: _textController, controller: _textController,
focusNode: widget.focusNode, focusNode: widget.focusNode,
inputFormatters: widget.maxLength != null inputFormatters: widget.textInputFormatter ??
? [LengthLimitingTextInputFormatter(50)] (widget.maxLength != null
: null, ? [LengthLimitingTextInputFormatter(50)]
: null),
obscureText: _obscureTextNotifier.value, obscureText: _obscureTextNotifier.value,
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.hintText, hintText: widget.hintText,
@ -343,6 +345,42 @@ class _TextInputWidgetState extends State<TextInputWidget> {
void _popNavigatorStack(BuildContext context, {Exception? e}) { void _popNavigatorStack(BuildContext context, {Exception? e}) {
Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null; 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 //todo: Add clear and custom icon for suffic icon

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,11 @@
import "package:flutter/material.dart"; 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/generated/l10n.dart";
import "package:photos/states/location_state.dart"; import "package:photos/states/location_state.dart";
import "package:photos/theme/colors.dart"; import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/ente_theme.dart";
import "package:photos/utils/dialog_util.dart";
class CustomTrackShape extends RoundedRectSliderTrackShape { class CustomTrackShape extends RoundedRectSliderTrackShape {
@override @override
@ -21,11 +23,11 @@ class CustomTrackShape extends RoundedRectSliderTrackShape {
} }
class RadiusPickerWidget extends StatefulWidget { class RadiusPickerWidget extends StatefulWidget {
///This notifier can be listened to get the selected radius index from ///This notifier can be listened from a parent widget to get the selected radius
///a parent widget. final ValueNotifier<double> selectedRadiusNotifier;
final ValueNotifier<int> selectedRadiusIndexNotifier;
const RadiusPickerWidget( const RadiusPickerWidget(
this.selectedRadiusIndexNotifier, { this.selectedRadiusNotifier, {
super.key, super.key,
}); });
@ -34,6 +36,7 @@ class RadiusPickerWidget extends StatefulWidget {
} }
class _RadiusPickerWidgetState extends State<RadiusPickerWidget> { class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
final _logger = Logger("RadiusPickerWidget");
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -41,51 +44,62 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
widget.selectedRadiusIndexNotifier.value = widget.selectedRadiusNotifier.value =
InheritedLocationTagData.of(context).selectedRadiusIndex; InheritedLocationTagData.of(context).selectedRadius;
super.didChangeDependencies(); super.didChangeDependencies();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedRadiusIndex = widget.selectedRadiusIndexNotifier.value; final radiusValues = InheritedLocationTagData.of(context).radiusValues;
final radiusValue = radiusValues[selectedRadiusIndex]; final selectedRadius = widget.selectedRadiusNotifier.value;
final textTheme = getEnteTextTheme(context); final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context); final colorScheme = getEnteColorScheme(context);
final roundedRadius = roundRadius(selectedRadius);
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( GestureDetector(
height: 48, onTap: _customRadiusOnTap,
width: 48, child: Container(
decoration: BoxDecoration( height: 52,
color: colorScheme.fillFaint, width: 52,
borderRadius: const BorderRadius.all(Radius.circular(2)), decoration: BoxDecoration(
), color: colorScheme.fillFaint,
padding: const EdgeInsets.all(4), borderRadius: const BorderRadius.all(Radius.circular(2)),
child: Column( border: Border.all(
mainAxisSize: MainAxisSize.min, color: colorScheme.strokeFainter,
mainAxisAlignment: MainAxisAlignment.center, width: 1,
crossAxisAlignment: CrossAxisAlignment.center, strokeAlign: BorderSide.strokeAlignInside,
children: [
Expanded(
flex: 6,
child: Text(
radiusValue.toString(),
style: radiusValue != 1200
? textTheme.largeBold
: textTheme.bodyBold,
textAlign: TextAlign.center,
),
), ),
Expanded( ),
flex: 5, padding: const EdgeInsets.all(4),
child: Text( child: Column(
S.of(context).kiloMeterUnit, mainAxisSize: MainAxisSize.min,
style: textTheme.miniMuted, 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), const SizedBox(width: 4),
@ -96,6 +110,7 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 4),
Text(S.of(context).radius, style: textTheme.body), Text(S.of(context).radius, style: textTheme.body),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( SizedBox(
@ -120,11 +135,11 @@ class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
), ),
child: RepaintBoundary( child: RepaintBoundary(
child: Slider( child: Slider(
value: selectedRadiusIndex.toDouble(), value: radiusValues.indexOf(selectedRadius).toDouble(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
widget.selectedRadiusIndexNotifier.value = widget.selectedRadiusNotifier.value =
value.toInt(); radiusValues[value.toInt()];
}); });
}, },
min: 0, 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:confetti/confetti.dart';
import "package:dio/dio.dart"; import "package:dio/dio.dart";
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import "package:flutter/services.dart";
import 'package:photos/core/constants.dart'; import 'package:photos/core/constants.dart';
import "package:photos/generated/l10n.dart"; import "package:photos/generated/l10n.dart";
import "package:photos/models/search/button_result.dart"; import "package:photos/models/search/button_result.dart";
@ -303,6 +304,9 @@ Future<dynamic> showTextInputDialog(
TextCapitalization textCapitalization = TextCapitalization.none, TextCapitalization textCapitalization = TextCapitalization.none,
bool alwaysShowSuccessState = false, bool alwaysShowSuccessState = false,
bool isPasswordInput = false, bool isPasswordInput = false,
TextEditingController? textEditingController,
List<TextInputFormatter>? textInputFormatter,
TextInputType? textInputType,
}) { }) {
return showDialog( return showDialog(
barrierColor: backdropFaintDark, barrierColor: backdropFaintDark,
@ -330,6 +334,9 @@ Future<dynamic> showTextInputDialog(
textCapitalization: textCapitalization, textCapitalization: textCapitalization,
alwaysShowSuccessState: alwaysShowSuccessState, alwaysShowSuccessState: alwaysShowSuccessState,
isPasswordInput: isPasswordInput, isPasswordInput: isPasswordInput,
textEditingController: textEditingController,
textInputFormatter: textInputFormatter,
textInputType: textInputType,
), ),
), ),
); );