Add location screen (#941)

This commit is contained in:
Vishnu Mohandas 2023-03-28 13:37:43 +05:30 committed by GitHub
commit 90240f48b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 783 additions and 79 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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());

View 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;
}
}

View file

@ -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);

View file

@ -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(),

View file

@ -158,8 +158,9 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
_searchQuery = value;
});
},
cancellable: true,
shouldUnfocusOnCancelOrSubmit: true,
isClearable: true,
shouldUnfocusOnClearOrSubmit: true,
borderRadius: 2,
),
),
_getCollectionItems(filesCount),

View file

@ -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,
),
);
}

View file

@ -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();
}
},

View file

@ -372,7 +372,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
color: Colors.white, //same for both themes
),
onPressed: () {
showInfoSheet(context, file);
showDetailsSheet(context, file);
},
),
IconButton(

View file

@ -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,

View file

@ -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());
}
}
}

View file

@ -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);
}
}

View 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();
}

View file

@ -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;
}
}

View file

@ -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,
);

View 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,
);
}
}
}

View 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();
}
}
}

View 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,
),
),
),
),
],
),
),
),
],
);
}
}