Add location screen (#941)
This commit is contained in:
commit
90240f48b0
20 changed files with 783 additions and 79 deletions
|
@ -59,3 +59,9 @@ const double mobileSmallThreshold = 336;
|
|||
const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];
|
||||
|
||||
const kilometersPerDegree = 111.16;
|
||||
|
||||
const radiusValues = <int>[2, 10, 20, 40, 80, 200, 400, 1200];
|
||||
|
||||
const defaultRadiusValueIndex = 4;
|
||||
|
||||
const galleryGridSpacing = 2.0;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:developer' as dev;
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -485,14 +486,20 @@ class FilesDB {
|
|||
bool? asc,
|
||||
int visibility = visibilityVisible,
|
||||
Set<int>? ignoredCollectionIDs,
|
||||
bool onlyFilesWithLocation = false,
|
||||
}) async {
|
||||
final stopWatch = Stopwatch()..start();
|
||||
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
' AND $columnMMdVisibility = ?',
|
||||
where: onlyFilesWithLocation
|
||||
? '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
|
||||
'AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
'AND $columnMMdVisibility = ?'
|
||||
: '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)'
|
||||
' AND $columnMMdVisibility = ?',
|
||||
whereArgs: [startTime, endTime, ownerID, visibility],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
|
@ -501,6 +508,9 @@ class FilesDB {
|
|||
final files = convertToFiles(results);
|
||||
final List<File> deduplicatedFiles =
|
||||
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
|
||||
dev.log(
|
||||
"getAllPendingOrUploadedFiles time taken: ${stopWatch.elapsedMilliseconds} ms");
|
||||
stopWatch.stop();
|
||||
return FileLoadResult(deduplicatedFiles, files.length == limit);
|
||||
}
|
||||
|
||||
|
@ -511,14 +521,18 @@ class FilesDB {
|
|||
int? limit,
|
||||
bool? asc,
|
||||
Set<int>? ignoredCollectionIDs,
|
||||
bool onlyFilesWithLocation = false,
|
||||
}) async {
|
||||
final db = await instance.database;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where:
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
where: onlyFilesWithLocation
|
||||
? '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)'
|
||||
'$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))'
|
||||
: '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)'
|
||||
' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))',
|
||||
whereArgs: [startTime, endTime, ownerID, visibilityVisible],
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
|
@ -1387,6 +1401,10 @@ class FilesDB {
|
|||
row[columnCollectionID] = file.collectionID ?? -1;
|
||||
row[columnTitle] = file.title;
|
||||
row[columnDeviceFolder] = file.deviceFolder;
|
||||
// if (file.location == null ||
|
||||
// (file.location!.latitude == null && file.location!.longitude == null)) {
|
||||
// file.location = Location.randomLocation();
|
||||
// }
|
||||
if (file.location != null) {
|
||||
row[columnLatitude] = file.location!.latitude;
|
||||
row[columnLongitude] = file.location!.longitude;
|
||||
|
|
|
@ -5,3 +5,4 @@ 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);
|
||||
|
|
|
@ -73,6 +73,21 @@ class LocationService {
|
|||
return result;
|
||||
}
|
||||
|
||||
bool isFileInsideLocationTag(
|
||||
List<double> center,
|
||||
List<double> fileCoordinates,
|
||||
int radius,
|
||||
) {
|
||||
final a = (radius * _scaleFactor(center[0])) / kilometersPerDegree;
|
||||
final b = radius / kilometersPerDegree;
|
||||
final x = center[0] - fileCoordinates[0];
|
||||
final y = center[1] - fileCoordinates[1];
|
||||
if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> addFileToLocation(int locationId, int fileId) async {
|
||||
final list = getFilesByLocation(locationId.toString());
|
||||
list.add(fileId.toString());
|
||||
|
|
71
lib/states/add_location_state.dart
Normal file
71
lib/states/add_location_state.dart
Normal file
|
@ -0,0 +1,71 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/models/typedefs.dart";
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
|
||||
class LocationTagDataStateProvider extends StatefulWidget {
|
||||
final List<double> coordinates;
|
||||
final Widget child;
|
||||
const LocationTagDataStateProvider(this.coordinates, this.child, {super.key});
|
||||
|
||||
@override
|
||||
State<LocationTagDataStateProvider> createState() =>
|
||||
_LocationTagDataStateProviderState();
|
||||
}
|
||||
|
||||
class _LocationTagDataStateProviderState
|
||||
extends State<LocationTagDataStateProvider> {
|
||||
int selectedRaduisIndex = defaultRadiusValueIndex;
|
||||
late List<double> coordinates;
|
||||
final Debouncer _selectedRadiusDebouncer =
|
||||
Debouncer(const Duration(milliseconds: 300));
|
||||
@override
|
||||
void initState() {
|
||||
coordinates = widget.coordinates;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _updateSelectedIndex(int index) {
|
||||
_selectedRadiusDebouncer.cancelDebounce();
|
||||
_selectedRadiusDebouncer.run(() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
selectedRaduisIndex = index;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InheritedLocationTagData(
|
||||
selectedRaduisIndex,
|
||||
coordinates,
|
||||
_updateSelectedIndex,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedLocationTagData extends InheritedWidget {
|
||||
final int selectedRadiusIndex;
|
||||
final List<double> coordinates;
|
||||
final VoidCallbackParamInt updateSelectedIndex;
|
||||
const InheritedLocationTagData(
|
||||
this.selectedRadiusIndex,
|
||||
this.coordinates,
|
||||
this.updateSelectedIndex, {
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static InheritedLocationTagData of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedLocationTagData>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedLocationTagData oldWidget) {
|
||||
return oldWidget.selectedRadiusIndex != selectedRadiusIndex;
|
||||
}
|
||||
}
|
|
@ -216,6 +216,7 @@ const Color tabIconDark = Color.fromRGBO(255, 255, 255, 0.80);
|
|||
// Fixed Colors
|
||||
|
||||
const Color fixedStrokeMutedWhite = Color.fromRGBO(255, 255, 255, 0.50);
|
||||
const Color strokeSolidMutedLight = Color.fromRGBO(147, 147, 147, 1);
|
||||
|
||||
const Color _primary700 = Color.fromRGBO(0, 179, 60, 1);
|
||||
const Color _primary500 = Color.fromRGBO(29, 185, 84, 1);
|
||||
|
|
|
@ -124,7 +124,7 @@ Future<void> showSingleFileDeleteSheet(
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> showInfoSheet(BuildContext context, File file) async {
|
||||
Future<void> showDetailsSheet(BuildContext context, File file) async {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return showBarModalBottomSheet(
|
||||
topControl: const SizedBox.shrink(),
|
||||
|
|
|
@ -158,8 +158,9 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
|||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
cancellable: true,
|
||||
shouldUnfocusOnCancelOrSubmit: true,
|
||||
isClearable: true,
|
||||
shouldUnfocusOnClearOrSubmit: true,
|
||||
borderRadius: 2,
|
||||
),
|
||||
),
|
||||
_getCollectionItems(filesCount),
|
||||
|
|
|
@ -28,17 +28,23 @@ class DividerWidget extends StatelessWidget {
|
|||
: getEnteColorScheme(context).strokeFaint;
|
||||
|
||||
if (dividerType == DividerType.solid) {
|
||||
return Container(
|
||||
color: getEnteColorScheme(context).strokeFaint,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
color: getEnteColorScheme(context).strokeFaint,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (dividerType == DividerType.bottomBar) {
|
||||
return Container(
|
||||
color: dividerColor,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
color: dividerColor,
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,15 @@ class TextInputWidget extends StatefulWidget {
|
|||
final Alignment? alignMessage;
|
||||
final bool? autoFocus;
|
||||
final int? maxLength;
|
||||
final double borderRadius;
|
||||
|
||||
///TextInputWidget will listen to this notifier and executes onSubmit when
|
||||
///notified.
|
||||
final ValueNotifier? submitNotifier;
|
||||
|
||||
///TextInputWidget will listen to this notifier and clears and unfocuses the
|
||||
///textFiled when notified.
|
||||
final ValueNotifier? cancelNotifier;
|
||||
final bool alwaysShowSuccessState;
|
||||
final bool showOnlyLoadingState;
|
||||
final FutureVoidCallbackParamStr? onSubmit;
|
||||
|
@ -28,8 +33,11 @@ class TextInputWidget extends StatefulWidget {
|
|||
final bool shouldSurfaceExecutionStates;
|
||||
final TextCapitalization? textCapitalization;
|
||||
final bool isPasswordInput;
|
||||
final bool cancellable;
|
||||
final bool shouldUnfocusOnCancelOrSubmit;
|
||||
|
||||
///Clear comes in the form of a suffix icon. It is unrelated to onCancel.
|
||||
final bool isClearable;
|
||||
final bool shouldUnfocusOnClearOrSubmit;
|
||||
final FocusNode? focusNode;
|
||||
const TextInputWidget({
|
||||
this.onSubmit,
|
||||
this.onChange,
|
||||
|
@ -42,14 +50,17 @@ class TextInputWidget extends StatefulWidget {
|
|||
this.autoFocus,
|
||||
this.maxLength,
|
||||
this.submitNotifier,
|
||||
this.cancelNotifier,
|
||||
this.alwaysShowSuccessState = false,
|
||||
this.showOnlyLoadingState = false,
|
||||
this.popNavAfterSubmission = false,
|
||||
this.shouldSurfaceExecutionStates = true,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.isPasswordInput = false,
|
||||
this.cancellable = false,
|
||||
this.shouldUnfocusOnCancelOrSubmit = false,
|
||||
this.isClearable = false,
|
||||
this.shouldUnfocusOnClearOrSubmit = false,
|
||||
this.borderRadius = 8,
|
||||
this.focusNode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -70,6 +81,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
@override
|
||||
void initState() {
|
||||
widget.submitNotifier?.addListener(_onSubmit);
|
||||
widget.cancelNotifier?.addListener(_onCancel);
|
||||
|
||||
if (widget.initialValue != null) {
|
||||
_textController.value = TextEditingValue(
|
||||
|
@ -90,6 +102,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
@override
|
||||
void dispose() {
|
||||
widget.submitNotifier?.removeListener(_onSubmit);
|
||||
widget.cancelNotifier?.removeListener(_onCancel);
|
||||
_obscureTextNotifier.dispose();
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
|
@ -113,12 +126,13 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
}
|
||||
textInputChildren.add(
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius)),
|
||||
child: Material(
|
||||
child: TextFormField(
|
||||
textCapitalization: widget.textCapitalization!,
|
||||
autofocus: widget.autoFocus ?? false,
|
||||
controller: _textController,
|
||||
focusNode: widget.focusNode,
|
||||
inputFormatters: widget.maxLength != null
|
||||
? [LengthLimitingTextInputFormatter(50)]
|
||||
: null,
|
||||
|
@ -155,9 +169,9 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
obscureTextNotifier: _obscureTextNotifier,
|
||||
isPasswordInput: widget.isPasswordInput,
|
||||
textController: _textController,
|
||||
isCancellable: widget.cancellable,
|
||||
shouldUnfocusOnCancelOrSubmit:
|
||||
widget.shouldUnfocusOnCancelOrSubmit,
|
||||
isClearable: widget.isClearable,
|
||||
shouldUnfocusOnClearOrSubmit:
|
||||
widget.shouldUnfocusOnClearOrSubmit,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -224,7 +238,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
});
|
||||
}),
|
||||
);
|
||||
if (widget.shouldUnfocusOnCancelOrSubmit) {
|
||||
if (widget.shouldUnfocusOnClearOrSubmit) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
try {
|
||||
|
@ -303,6 +317,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onCancel() {
|
||||
_textController.clear();
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
void _popNavigatorStack(BuildContext context, {Exception? e}) {
|
||||
Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null;
|
||||
}
|
||||
|
@ -315,8 +334,8 @@ class SuffixIconWidget extends StatelessWidget {
|
|||
final TextEditingController textController;
|
||||
final ValueNotifier? obscureTextNotifier;
|
||||
final bool isPasswordInput;
|
||||
final bool isCancellable;
|
||||
final bool shouldUnfocusOnCancelOrSubmit;
|
||||
final bool isClearable;
|
||||
final bool shouldUnfocusOnClearOrSubmit;
|
||||
|
||||
const SuffixIconWidget({
|
||||
required this.executionState,
|
||||
|
@ -324,8 +343,8 @@ class SuffixIconWidget extends StatelessWidget {
|
|||
required this.textController,
|
||||
this.obscureTextNotifier,
|
||||
this.isPasswordInput = false,
|
||||
this.isCancellable = false,
|
||||
this.shouldUnfocusOnCancelOrSubmit = false,
|
||||
this.isClearable = false,
|
||||
this.shouldUnfocusOnClearOrSubmit = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
@ -335,11 +354,11 @@ class SuffixIconWidget extends StatelessWidget {
|
|||
final colorScheme = getEnteColorScheme(context);
|
||||
if (executionState == ExecutionState.idle ||
|
||||
!shouldSurfaceExecutionStates) {
|
||||
if (isCancellable) {
|
||||
if (isClearable) {
|
||||
trailingWidget = GestureDetector(
|
||||
onTap: () {
|
||||
textController.clear();
|
||||
if (shouldUnfocusOnCancelOrSubmit) {
|
||||
if (shouldUnfocusOnClearOrSubmit) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -372,7 +372,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
|
|||
color: Colors.white, //same for both themes
|
||||
),
|
||||
onPressed: () {
|
||||
showInfoSheet(context, file);
|
||||
showDetailsSheet(context, file);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
|
|
|
@ -60,6 +60,8 @@ class HugeListView<T> extends StatefulWidget {
|
|||
|
||||
final EdgeInsetsGeometry? thumbPadding;
|
||||
|
||||
final bool disableScroll;
|
||||
|
||||
const HugeListView({
|
||||
Key? key,
|
||||
this.controller,
|
||||
|
@ -77,6 +79,7 @@ class HugeListView<T> extends StatefulWidget {
|
|||
this.bottomSafeArea = 120.0,
|
||||
this.isDraggableScrollbarEnabled = true,
|
||||
this.thumbPadding,
|
||||
this.disableScroll = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -160,6 +163,9 @@ class HugeListViewState<T> extends State<HugeListView<T>> {
|
|||
isEnabled: widget.isDraggableScrollbarEnabled,
|
||||
padding: widget.thumbPadding,
|
||||
child: ScrollablePositionedList.builder(
|
||||
physics: widget.disableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
itemScrollController: widget.controller,
|
||||
itemPositionsListener: listener,
|
||||
initialScrollIndex: widget.startIndex,
|
||||
|
|
|
@ -32,11 +32,12 @@ class LazyLoadingGallery extends StatefulWidget {
|
|||
final Stream<FilesUpdatedEvent>? reloadEvent;
|
||||
final Set<EventType> removalEventTypes;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles selectedFiles;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final String tag;
|
||||
final String? logTag;
|
||||
final Stream<int> currentIndexStream;
|
||||
final int photoGirdSize;
|
||||
final bool areFilesCollatedByDay;
|
||||
LazyLoadingGallery(
|
||||
this.files,
|
||||
this.index,
|
||||
|
@ -45,7 +46,8 @@ class LazyLoadingGallery extends StatefulWidget {
|
|||
this.asyncLoader,
|
||||
this.selectedFiles,
|
||||
this.tag,
|
||||
this.currentIndexStream, {
|
||||
this.currentIndexStream,
|
||||
this.areFilesCollatedByDay, {
|
||||
this.logTag = "",
|
||||
this.photoGirdSize = photoGridSizeDefault,
|
||||
Key? key,
|
||||
|
@ -62,7 +64,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
late Logger _logger;
|
||||
|
||||
late List<File> _files;
|
||||
late StreamSubscription<FilesUpdatedEvent> _reloadEventSubscription;
|
||||
late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
|
||||
late StreamSubscription<int> _currentIndexSubscription;
|
||||
bool? _shouldRender;
|
||||
final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
|
||||
|
@ -72,7 +74,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
@override
|
||||
void initState() {
|
||||
//this is for removing the 'select all from day' icon on unselecting all files with 'cancel'
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
@ -81,7 +83,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
_logger = Logger("LazyLoading_${widget.logTag}");
|
||||
_shouldRender = true;
|
||||
_files = widget.files;
|
||||
_reloadEventSubscription = widget.reloadEvent!.listen((e) => _onReload(e));
|
||||
_reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
|
||||
|
||||
_currentIndexSubscription =
|
||||
widget.currentIndexStream.listen((currentIndex) {
|
||||
|
@ -162,9 +164,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_reloadEventSubscription.cancel();
|
||||
_reloadEventSubscription?.cancel();
|
||||
_currentIndexSubscription.cancel();
|
||||
widget.selectedFiles.removeListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_toggleSelectAllFromDay.dispose();
|
||||
_showSelectAllButton.dispose();
|
||||
_areAllFromDaySelected.dispose();
|
||||
|
@ -175,7 +177,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
void didUpdateWidget(LazyLoadingGallery oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!listEquals(_files, widget.files)) {
|
||||
_reloadEventSubscription.cancel();
|
||||
_reloadEventSubscription?.cancel();
|
||||
_init();
|
||||
}
|
||||
}
|
||||
|
@ -190,11 +192,12 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
getDayWidget(
|
||||
context,
|
||||
_files[0].creationTime!,
|
||||
widget.photoGirdSize,
|
||||
),
|
||||
if (widget.areFilesCollatedByDay)
|
||||
getDayWidget(
|
||||
context,
|
||||
_files[0].creationTime!,
|
||||
widget.photoGirdSize,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _showSelectAllButton,
|
||||
builder: (context, dynamic value, _) {
|
||||
|
@ -271,7 +274,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
|||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles.files.isEmpty) {
|
||||
if (widget.selectedFiles!.files.isEmpty) {
|
||||
_showSelectAllButton.value = false;
|
||||
} else {
|
||||
_showSelectAllButton.value = true;
|
||||
|
@ -283,7 +286,7 @@ class LazyLoadingGridView extends StatefulWidget {
|
|||
final String tag;
|
||||
final List<File> filesInDay;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles selectedFiles;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final bool shouldRender;
|
||||
final bool shouldRecycle;
|
||||
final ValueNotifier toggleSelectAllFromDay;
|
||||
|
@ -316,7 +319,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
void initState() {
|
||||
_shouldRender = widget.shouldRender;
|
||||
_currentUserID = Configuration.instance.getUserID();
|
||||
widget.selectedFiles.addListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent =
|
||||
Bus.instance.on<ClearSelectionsEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
|
@ -329,7 +332,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.selectedFiles.removeListener(_selectedFilesListener);
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent.cancel();
|
||||
widget.toggleSelectAllFromDay
|
||||
.removeListener(_toggleSelectAllFromDayListener);
|
||||
|
@ -403,12 +406,12 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
mainAxisSpacing: 2,
|
||||
crossAxisCount: widget.photoGridSize!,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFile(BuildContext context, File file) {
|
||||
final isFileSelected = widget.selectedFiles.isFileSelected(file);
|
||||
final isFileSelected = widget.selectedFiles?.isFileSelected(file) ?? false;
|
||||
Color selectionColor = Colors.white;
|
||||
if (isFileSelected &&
|
||||
file.isUploaded &&
|
||||
|
@ -422,7 +425,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (widget.selectedFiles.files.isNotEmpty) {
|
||||
if (widget.selectedFiles?.files.isNotEmpty ?? false) {
|
||||
_selectFile(file);
|
||||
} else {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
|
@ -434,13 +437,15 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_selectFile(file);
|
||||
}
|
||||
},
|
||||
onLongPress: widget.selectedFiles != null
|
||||
? () {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_selectFile(file);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
child: Stack(
|
||||
|
@ -486,7 +491,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
|
||||
void _selectFile(File file) {
|
||||
widget.selectedFiles.toggleSelection(file);
|
||||
widget.selectedFiles!.toggleSelection(file);
|
||||
}
|
||||
|
||||
void _routeToDetailPage(File file, BuildContext context) {
|
||||
|
@ -502,14 +507,14 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles.files.containsAll(widget.filesInDay.toSet())) {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
widget.areAllFilesSelected.value = true;
|
||||
} else {
|
||||
widget.areAllFilesSelected.value = false;
|
||||
}
|
||||
bool shouldRefresh = false;
|
||||
for (final file in widget.filesInDay) {
|
||||
if (widget.selectedFiles.isPartOfLastSelected(file)) {
|
||||
if (widget.selectedFiles!.isPartOfLastSelected(file)) {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
}
|
||||
|
@ -519,12 +524,12 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
|||
}
|
||||
|
||||
void _toggleSelectAllFromDayListener() {
|
||||
if (widget.selectedFiles.files.containsAll(widget.filesInDay.toSet())) {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
setState(() {
|
||||
widget.selectedFiles.unSelectAll(widget.filesInDay.toSet());
|
||||
widget.selectedFiles!.unSelectAll(widget.filesInDay.toSet());
|
||||
});
|
||||
} else {
|
||||
widget.selectedFiles.selectAll(widget.filesInDay.toSet());
|
||||
widget.selectedFiles!.selectAll(widget.filesInDay.toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ class FadingBottomBarState extends State<FadingBottomBar> {
|
|||
color: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
await _displayInfo(widget.file);
|
||||
await _displayDetails(widget.file);
|
||||
safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
|
@ -267,7 +267,7 @@ class FadingBottomBarState extends State<FadingBottomBar> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _displayInfo(File file) async {
|
||||
await showInfoSheet(context, file);
|
||||
Future<void> _displayDetails(File file) async {
|
||||
await showDetailsSheet(context, file);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
|||
final _focusNode = FocusNode();
|
||||
String? editedCaption;
|
||||
String hintText = fileCaptionDefaultHint;
|
||||
Widget? keyboardTopButtoms;
|
||||
Widget? keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -172,12 +172,12 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
|||
editedCaption = caption;
|
||||
}
|
||||
final bool hasFocus = _focusNode.hasFocus;
|
||||
keyboardTopButtoms ??= KeyboardTopButton(
|
||||
keyboardTopButtons ??= KeyboardTopButton(
|
||||
onDoneTap: onDoneTap,
|
||||
onCancelTap: onCancelTap,
|
||||
);
|
||||
if (hasFocus) {
|
||||
KeyboardOverlay.showOverlay(context, keyboardTopButtoms!);
|
||||
KeyboardOverlay.showOverlay(context, keyboardTopButtons!);
|
||||
} else {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import "package:photos/services/location_service.dart";
|
|||
import "package:photos/ui/components/buttons/chip_button_widget.dart";
|
||||
import "package:photos/ui/components/buttons/inline_button_widget.dart";
|
||||
import "package:photos/ui/components/info_item_widget.dart";
|
||||
import 'package:photos/ui/viewer/location/add_location_sheet.dart';
|
||||
|
||||
class LocationTagsWidget extends StatefulWidget {
|
||||
final List<double> coordinates;
|
||||
|
@ -18,7 +19,7 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
String title = "Add location";
|
||||
IconData leadingIcon = Icons.add_location_alt_outlined;
|
||||
bool hasChipButtons = false;
|
||||
late final Future<List<Widget>> locationTagChips;
|
||||
late Future<List<Widget>> locationTagChips;
|
||||
@override
|
||||
void initState() {
|
||||
locationTagChips = _getLocationTags();
|
||||
|
@ -46,7 +47,18 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
LocationService.instance.enclosingLocationTags(widget.coordinates);
|
||||
if (locationTags.isEmpty) {
|
||||
return [
|
||||
InlineButtonWidget("Group nearby photos", () {}),
|
||||
InlineButtonWidget(
|
||||
"Group nearby photos",
|
||||
() => showAddLocationSheet(
|
||||
context,
|
||||
widget.coordinates,
|
||||
//This callback is for reloading the locationTagsWidget after adding a new location tag
|
||||
//so that it updates in file details.
|
||||
() {
|
||||
locationTagChips = _getLocationTags();
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
setState(() {
|
||||
|
@ -54,6 +66,22 @@ class _LocationTagsWidgetState extends State<LocationTagsWidget> {
|
|||
leadingIcon = Icons.pin_drop_outlined;
|
||||
hasChipButtons = true;
|
||||
});
|
||||
return locationTags.map((e) => ChipButtonWidget(e)).toList();
|
||||
final result = locationTags.map((e) => ChipButtonWidget(e)).toList();
|
||||
result.add(
|
||||
ChipButtonWidget(
|
||||
null,
|
||||
leadingIcon: Icons.add_outlined,
|
||||
onTap: () => showAddLocationSheet(
|
||||
context,
|
||||
widget.coordinates,
|
||||
//This callback is for reloading the locationTagsWidget after adding a new location tag
|
||||
//so that it updates in file details.
|
||||
() {
|
||||
locationTagChips = _getLocationTags();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,18 +33,21 @@ class Gallery extends StatefulWidget {
|
|||
final Stream<FilesUpdatedEvent>? reloadEvent;
|
||||
final List<Stream<Event>>? forceReloadEvents;
|
||||
final Set<EventType> removalEventTypes;
|
||||
final SelectedFiles selectedFiles;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final String tagPrefix;
|
||||
final Widget? header;
|
||||
final Widget? footer;
|
||||
final Widget emptyState;
|
||||
final String? albumName;
|
||||
final double scrollBottomSafeArea;
|
||||
final bool shouldCollateFilesByDay;
|
||||
final Widget loadingWidget;
|
||||
final bool disableScroll;
|
||||
|
||||
const Gallery({
|
||||
required this.asyncLoader,
|
||||
required this.selectedFiles,
|
||||
required this.tagPrefix,
|
||||
this.selectedFiles,
|
||||
this.initialFiles,
|
||||
this.reloadEvent,
|
||||
this.forceReloadEvents,
|
||||
|
@ -54,6 +57,9 @@ class Gallery extends StatefulWidget {
|
|||
this.emptyState = const EmptyState(),
|
||||
this.scrollBottomSafeArea = 120.0,
|
||||
this.albumName = '',
|
||||
this.shouldCollateFilesByDay = true,
|
||||
this.loadingWidget = const EnteLoadingWidget(),
|
||||
this.disableScroll = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -168,7 +174,8 @@ class _GalleryState extends State<Gallery> {
|
|||
|
||||
// Collates files and returns `true` if it resulted in a gallery reload
|
||||
bool _onFilesLoaded(List<File> files) {
|
||||
final updatedCollatedFiles = _collateFiles(files);
|
||||
final updatedCollatedFiles =
|
||||
widget.shouldCollateFilesByDay ? _collateFiles(files) : [files];
|
||||
if (_collatedFiles.length != updatedCollatedFiles.length ||
|
||||
_collatedFiles.isEmpty) {
|
||||
if (mounted) {
|
||||
|
@ -198,7 +205,7 @@ class _GalleryState extends State<Gallery> {
|
|||
Widget build(BuildContext context) {
|
||||
_logger.finest("Building Gallery ${widget.tagPrefix}");
|
||||
if (!_hasLoadedFiles) {
|
||||
return const EnteLoadingWidget();
|
||||
return widget.loadingWidget;
|
||||
}
|
||||
_photoGridSize = LocalSettings.instance.getPhotoGridSize();
|
||||
return _getListView();
|
||||
|
@ -211,6 +218,7 @@ class _GalleryState extends State<Gallery> {
|
|||
startIndex: 0,
|
||||
totalCount: _collatedFiles.length,
|
||||
isDraggableScrollbarEnabled: _collatedFiles.length > 10,
|
||||
disableScroll: widget.disableScroll,
|
||||
waitBuilder: (_) {
|
||||
return const EnteLoadingWidget();
|
||||
},
|
||||
|
@ -246,6 +254,7 @@ class _GalleryState extends State<Gallery> {
|
|||
.on<GalleryIndexUpdatedEvent>()
|
||||
.where((event) => event.tag == widget.tagPrefix)
|
||||
.map((event) => event.index),
|
||||
widget.shouldCollateFilesByDay,
|
||||
logTag: _logTag,
|
||||
photoGirdSize: _photoGridSize,
|
||||
);
|
||||
|
|
168
lib/ui/viewer/location/add_location_gallery_widget.dart
Normal file
168
lib/ui/viewer/location/add_location_gallery_widget.dart
Normal file
|
@ -0,0 +1,168 @@
|
|||
import "dart:developer" as dev;
|
||||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/file_load_result.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/add_location_state.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
|
||||
class AddLocationGalleryWidget extends StatefulWidget {
|
||||
final ValueNotifier<int?> memoriesCountNotifier;
|
||||
const AddLocationGalleryWidget(this.memoriesCountNotifier, {super.key});
|
||||
|
||||
@override
|
||||
State<AddLocationGalleryWidget> createState() =>
|
||||
_AddLocationGalleryWidgetState();
|
||||
}
|
||||
|
||||
class _AddLocationGalleryWidgetState extends State<AddLocationGalleryWidget> {
|
||||
late final Future<FileLoadResult> fileLoadResult;
|
||||
late Future<void> removeIgnoredFiles;
|
||||
double heightOfGallery = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
fileLoadResult = _fetchAllFilesWithLocationData();
|
||||
removeIgnoredFiles = _removeIgnoredFiles(fileLoadResult);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const galleryFilesLimit = 1000;
|
||||
final selectedRadius = _selectedRadius();
|
||||
Future<FileLoadResult> filterFiles() async {
|
||||
final FileLoadResult result = await fileLoadResult;
|
||||
//wait for ignored files to be removed after init
|
||||
await removeIgnoredFiles;
|
||||
final stopWatch = Stopwatch()..start();
|
||||
final copyOfFiles = List<File>.from(result.files);
|
||||
copyOfFiles.removeWhere((f) {
|
||||
assert(
|
||||
f.location != null &&
|
||||
f.location!.latitude != null &&
|
||||
f.location!.longitude != null,
|
||||
);
|
||||
return !LocationService.instance.isFileInsideLocationTag(
|
||||
InheritedLocationTagData.of(context).coordinates,
|
||||
[f.location!.latitude!, f.location!.longitude!],
|
||||
selectedRadius,
|
||||
);
|
||||
});
|
||||
dev.log(
|
||||
"Time taken to get all files in a location tag: ${stopWatch.elapsedMilliseconds} ms",
|
||||
);
|
||||
stopWatch.stop();
|
||||
widget.memoriesCountNotifier.value = copyOfFiles.length;
|
||||
final limitedResults = copyOfFiles.take(galleryFilesLimit).toList();
|
||||
|
||||
return Future.value(
|
||||
FileLoadResult(
|
||||
limitedResults,
|
||||
result.hasMore,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
key: ValueKey(selectedRadius),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return SizedBox(
|
||||
height: _galleryHeight(
|
||||
min(
|
||||
(widget.memoriesCountNotifier.value ?? 0),
|
||||
galleryFilesLimit,
|
||||
),
|
||||
),
|
||||
child: Gallery(
|
||||
key: ValueKey(selectedRadius),
|
||||
loadingWidget: const SizedBox.shrink(),
|
||||
disableScroll: true,
|
||||
asyncLoader: (
|
||||
creationStartTime,
|
||||
creationEndTime, {
|
||||
limit,
|
||||
asc,
|
||||
}) async {
|
||||
return snapshot.data as FileLoadResult;
|
||||
},
|
||||
tagPrefix: "Add location",
|
||||
shouldCollateFilesByDay: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
future: filterFiles(),
|
||||
);
|
||||
}
|
||||
|
||||
int _selectedRadius() {
|
||||
return radiusValues[
|
||||
InheritedLocationTagData.of(context).selectedRadiusIndex];
|
||||
}
|
||||
|
||||
Future<void> _removeIgnoredFiles(Future<FileLoadResult> result) async {
|
||||
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
|
||||
(await result).files.removeWhere(
|
||||
(f) =>
|
||||
f.uploadedFileID == null &&
|
||||
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
|
||||
);
|
||||
}
|
||||
|
||||
double _galleryHeight(int fileCount) {
|
||||
final photoGridSize = LocalSettings.instance.getPhotoGridSize();
|
||||
final totalWhiteSpaceBetweenPhotos =
|
||||
galleryGridSpacing * (photoGridSize - 1);
|
||||
|
||||
final thumbnailHeight =
|
||||
((MediaQuery.of(context).size.width - totalWhiteSpaceBetweenPhotos) /
|
||||
photoGridSize);
|
||||
|
||||
final numberOfRows = (fileCount / photoGridSize).ceil();
|
||||
|
||||
final galleryHeight = (thumbnailHeight * numberOfRows) +
|
||||
(galleryGridSpacing * (numberOfRows - 1));
|
||||
return galleryHeight + 120;
|
||||
}
|
||||
|
||||
Future<FileLoadResult> _fetchAllFilesWithLocationData() {
|
||||
final ownerID = Configuration.instance.getUserID();
|
||||
final hasSelectedAllForBackup =
|
||||
Configuration.instance.hasSelectedAllFoldersForBackup();
|
||||
final collectionsToHide =
|
||||
CollectionsService.instance.collectionsHiddenFromTimeline();
|
||||
if (hasSelectedAllForBackup) {
|
||||
return FilesDB.instance.getAllLocalAndUploadedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
ownerID!,
|
||||
limit: null,
|
||||
asc: true,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
onlyFilesWithLocation: true,
|
||||
);
|
||||
} else {
|
||||
return FilesDB.instance.getAllPendingOrUploadedFiles(
|
||||
galleryLoadStartTime,
|
||||
galleryLoadEndTime,
|
||||
ownerID!,
|
||||
limit: null,
|
||||
asc: true,
|
||||
ignoredCollectionIDs: collectionsToHide,
|
||||
onlyFilesWithLocation: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
216
lib/ui/viewer/location/add_location_sheet.dart
Normal file
216
lib/ui/viewer/location/add_location_sheet.dart
Normal file
|
@ -0,0 +1,216 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import "package:photos/states/add_location_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/bottom_of_title_bar_widget.dart";
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
import "package:photos/ui/components/keyboard/keybiard_oveylay.dart";
|
||||
import "package:photos/ui/components/keyboard/keyboard_top_button.dart";
|
||||
import "package:photos/ui/components/text_input_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/viewer/location/add_location_gallery_widget.dart";
|
||||
import "package:photos/ui/viewer/location/radius_picker_widget.dart";
|
||||
|
||||
showAddLocationSheet(
|
||||
BuildContext context,
|
||||
List<double> coordinates,
|
||||
VoidCallback onLocationAdded,
|
||||
) {
|
||||
showBarModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocationTagDataStateProvider(
|
||||
coordinates,
|
||||
AddLocationSheet(onLocationAdded),
|
||||
);
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
topControl: const SizedBox.shrink(),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
enableDrag: false,
|
||||
);
|
||||
}
|
||||
|
||||
class AddLocationSheet extends StatefulWidget {
|
||||
final VoidCallback onLocationAdded;
|
||||
const AddLocationSheet(this.onLocationAdded, {super.key});
|
||||
|
||||
@override
|
||||
State<AddLocationSheet> createState() => _AddLocationSheetState();
|
||||
}
|
||||
|
||||
class _AddLocationSheetState extends State<AddLocationSheet> {
|
||||
//The value of these notifiers 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);
|
||||
final ValueNotifier<bool> _submitNotifer = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _cancelNotifier = ValueNotifier(false);
|
||||
final _focusNode = FocusNode();
|
||||
Widget? _keyboardTopButtons;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
_submitNotifer.dispose();
|
||||
_cancelNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 32, 0, 8),
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: BottomOfTitleBarWidget(
|
||||
title: TitleBarTitleWidget(title: "Add location"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
TextInputWidget(
|
||||
hintText: "Location name",
|
||||
borderRadius: 2,
|
||||
focusNode: _focusNode,
|
||||
submitNotifier: _submitNotifer,
|
||||
cancelNotifier: _cancelNotifier,
|
||||
popNavAfterSubmission: true,
|
||||
onSubmit: (locationName) async {
|
||||
await _addLocationTag(locationName);
|
||||
},
|
||||
shouldUnfocusOnClearOrSubmit: true,
|
||||
alwaysShowSuccessState: true,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
RadiusPickerWidget(_memoriesCountNotifier),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"A location tag groups all photos that were taken within some radius of a photo",
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const DividerWidget(
|
||||
dividerType: DividerType.solid,
|
||||
padding: EdgeInsets.only(top: 24, bottom: 20),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _memoriesCountNotifier,
|
||||
builder: (context, value, _) {
|
||||
Widget widget;
|
||||
if (value == null) {
|
||||
widget = RepaintBoundary(
|
||||
child: EnteLoadingWidget(
|
||||
size: 14,
|
||||
color: colorScheme.strokeMuted,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: 3,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value == 1 ? "1 memory" : "$value memories",
|
||||
style: textTheme.body,
|
||||
),
|
||||
if (value as int > 1000)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
"Up to 1000 memories shown in gallery",
|
||||
style: textTheme.miniMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOutExpo,
|
||||
switchOutCurve: Curves.easeInOutExpo,
|
||||
child: widget,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
AddLocationGalleryWidget(_memoriesCountNotifier),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addLocationTag(String locationName) async {
|
||||
final locationData = InheritedLocationTagData.of(context);
|
||||
final coordinates = locationData.coordinates;
|
||||
final radius = radiusValues[locationData.selectedRadiusIndex];
|
||||
await LocationService.instance.addLocation(
|
||||
locationName,
|
||||
coordinates.first,
|
||||
coordinates.last,
|
||||
radius,
|
||||
);
|
||||
widget.onLocationAdded.call();
|
||||
}
|
||||
|
||||
void _focusNodeListener() {
|
||||
final bool hasFocus = _focusNode.hasFocus;
|
||||
_keyboardTopButtons ??= KeyboardTopButton(
|
||||
onDoneTap: () {
|
||||
_submitNotifer.value = !_submitNotifer.value;
|
||||
},
|
||||
onCancelTap: () {
|
||||
_cancelNotifier.value = !_cancelNotifier.value;
|
||||
},
|
||||
);
|
||||
if (hasFocus) {
|
||||
KeyboardOverlay.showOverlay(context, _keyboardTopButtons!);
|
||||
} else {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
}
|
||||
}
|
134
lib/ui/viewer/location/radius_picker_widget.dart
Normal file
134
lib/ui/viewer/location/radius_picker_widget.dart
Normal file
|
@ -0,0 +1,134 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/states/add_location_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
|
||||
class CustomTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
required SliderThemeData sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
const trackHeight = 2.0;
|
||||
final trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(0, 0, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
class RadiusPickerWidget extends StatefulWidget {
|
||||
final ValueNotifier<int?> memoriesCountNotifier;
|
||||
const RadiusPickerWidget(this.memoriesCountNotifier, {super.key});
|
||||
|
||||
@override
|
||||
State<RadiusPickerWidget> createState() => _RadiusPickerWidgetState();
|
||||
}
|
||||
|
||||
class _RadiusPickerWidgetState extends State<RadiusPickerWidget> {
|
||||
//Will maintain the state of the slider using this varialbe. Can't use
|
||||
//InheritedLocationData.selectedRadiusIndex as the state in the inheritedWidget
|
||||
//only changes after debounce time and the slider will not reflect the change immediately.
|
||||
int selectedRadiusIndex = defaultRadiusValueIndex;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radiusValue = radiusValues[selectedRadiusIndex];
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Row(
|
||||
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,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
"km",
|
||||
style: textTheme.miniMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Radius", style: textTheme.body),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 12,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
overlayColor: Colors.transparent,
|
||||
thumbColor: strokeSolidMutedLight,
|
||||
activeTrackColor: strokeSolidMutedLight,
|
||||
inactiveTrackColor: colorScheme.strokeFaint,
|
||||
activeTickMarkColor: colorScheme.strokeMuted,
|
||||
inactiveTickMarkColor: strokeSolidMutedLight,
|
||||
trackShape: CustomTrackShape(),
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6,
|
||||
pressedElevation: 0,
|
||||
elevation: 0,
|
||||
),
|
||||
tickMarkShape: const RoundSliderTickMarkShape(
|
||||
tickMarkRadius: 1,
|
||||
),
|
||||
),
|
||||
child: RepaintBoundary(
|
||||
child: Slider(
|
||||
value: selectedRadiusIndex.toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedRadiusIndex = value.toInt();
|
||||
});
|
||||
|
||||
InheritedLocationTagData.of(
|
||||
context,
|
||||
).updateSelectedIndex(
|
||||
value.toInt(),
|
||||
);
|
||||
widget.memoriesCountNotifier.value = null;
|
||||
},
|
||||
min: 0,
|
||||
max: radiusValues.length - 1,
|
||||
divisions: radiusValues.length - 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue