Custom radius (#1039)
This commit is contained in:
commit
299d1bb117
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue