123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- import 'dart:developer' as dev;
- import 'package:flutter/material.dart';
- import "package:modal_bottom_sheet/modal_bottom_sheet.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/models/typedefs.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/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/text_input_widget.dart";
- import "package:photos/ui/components/title_bar_title_widget.dart";
- import "package:photos/ui/viewer/gallery/gallery.dart";
- import "package:photos/utils/debouncer.dart";
- import "package:photos/utils/local_settings.dart";
- showAddLocationSheet(BuildContext context, List<double> coordinates) {
- showBarModalBottomSheet(
- context: context,
- builder: (context) {
- return LocationTagDataStateProvider(
- coordinates,
- const AddLocationSheet(),
- );
- },
- 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 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 selectedIndex = defaultRadiusValueIndex;
- late List<double> coordinates;
- final Debouncer _debouncer = Debouncer(const Duration(milliseconds: 300));
- @override
- void initState() {
- coordinates = widget.coordinates;
- super.initState();
- }
- void _updateSelectedIndex(int index) {
- _debouncer.cancelDebounce();
- _debouncer.run(() async {
- if (mounted) {
- setState(() {
- selectedIndex = index;
- });
- }
- });
- }
- @override
- Widget build(BuildContext context) {
- return InheritedLocationTagData(
- selectedIndex,
- coordinates,
- _updateSelectedIndex,
- child: widget.child,
- );
- }
- }
- class InheritedLocationTagData extends InheritedWidget {
- final int selectedIndex;
- final List<double> coordinates;
- final VoidCallbackParamInt updateSelectedIndex;
- const InheritedLocationTagData(
- this.selectedIndex,
- 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.selectedIndex != selectedIndex;
- }
- }
- class AddLocationSheet extends StatefulWidget {
- const AddLocationSheet({super.key});
- @override
- State<AddLocationSheet> createState() => _AddLocationSheetState();
- }
- class _AddLocationSheetState extends State<AddLocationSheet> {
- ValueNotifier<int?> memoriesCountNotifier = ValueNotifier(null);
- @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: [
- const TextInputWidget(
- hintText: "Location name",
- borderRadius: 2,
- ),
- 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 = Text(
- value == 1 ? "1 memory" : "$value memories",
- style: textTheme.body,
- );
- }
- return Align(
- alignment: Alignment.centerLeft,
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 250),
- switchInCurve: Curves.easeInOutExpo,
- switchOutCurve: Curves.easeInOutExpo,
- child: widget,
- ),
- );
- },
- ),
- ),
- ),
- const SizedBox(height: 24),
- AddToLocationGalleryWidget(memoriesCountNotifier),
- ],
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
- 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> {
- double selectedIndex = defaultRadiusValueIndex.toDouble();
- @override
- Widget build(BuildContext context) {
- 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(
- _selectedRadius(context).toInt().toString(),
- style: _selectedRadius(context) != 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: selectedIndex,
- onChanged: (value) {
- setState(() {
- selectedIndex = value;
- });
- InheritedLocationTagData.of(
- context,
- ).updateSelectedIndex(
- value.toInt(),
- );
- widget.memoriesCountNotifier.value = null;
- },
- min: 0,
- max: radiusValues.length - 1,
- divisions: radiusValues.length - 1,
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- );
- }
- double _selectedRadius(BuildContext context) {
- return radiusValues[InheritedLocationTagData.of(context).selectedIndex];
- }
- }
- class AddToLocationGalleryWidget extends StatefulWidget {
- final ValueNotifier<int?> memoriesCountNotifier;
- const AddToLocationGalleryWidget(this.memoriesCountNotifier, {super.key});
- @override
- State<AddToLocationGalleryWidget> createState() =>
- _AddToLocationGalleryWidgetState();
- }
- class _AddToLocationGalleryWidgetState
- extends State<AddToLocationGalleryWidget> {
- late final Future<FileLoadResult> fileLoadResult;
- late Future<void> removeIgnoredFiles;
- double heightOfGallery = 0;
- @override
- void initState() {
- final ownerID = Configuration.instance.getUserID();
- final hasSelectedAllForBackup =
- Configuration.instance.hasSelectedAllFoldersForBackup();
- final collectionsToHide =
- CollectionsService.instance.collectionsHiddenFromTimeline();
- if (hasSelectedAllForBackup) {
- fileLoadResult = FilesDB.instance.getAllLocalAndUploadedFiles(
- galleryLoadStartTime,
- galleryLoadEndTime,
- ownerID!,
- limit: null,
- asc: true,
- ignoredCollectionIDs: collectionsToHide,
- onlyFilesWithLocation: true,
- );
- } else {
- fileLoadResult = FilesDB.instance.getAllPendingOrUploadedFiles(
- galleryLoadStartTime,
- galleryLoadEndTime,
- ownerID!,
- limit: null,
- asc: true,
- ignoredCollectionIDs: collectionsToHide,
- onlyFilesWithLocation: true,
- );
- }
- removeIgnoredFiles = _removeIgnoredFiles(fileLoadResult);
- super.initState();
- }
- @override
- Widget build(BuildContext context) {
- final selectedRadius = _selectedRadius().toInt();
- late final int memoryCount;
- 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();
- memoryCount = copyOfFiles.length;
- widget.memoriesCountNotifier.value = copyOfFiles.length;
- return Future.value(
- FileLoadResult(
- copyOfFiles,
- result.hasMore,
- ),
- );
- }
- return FutureBuilder(
- key: ValueKey(selectedRadius),
- builder: (context, snapshot) {
- if (snapshot.hasData) {
- return SizedBox(
- height: _galleryHeight(memoryCount),
- 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(),
- );
- }
- double _selectedRadius() {
- return radiusValues[InheritedLocationTagData.of(context).selectedIndex];
- }
- 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 memoryCount) {
- final photoGridSize = LocalSettings.instance.getPhotoGridSize();
- final totalWhiteSpaceBetweenPhotos =
- galleryGridSpacing * (photoGridSize - 1);
- final thumbnailHeight =
- ((MediaQuery.of(context).size.width - totalWhiteSpaceBetweenPhotos) /
- photoGridSize);
- final numberOfRows = (memoryCount / photoGridSize).ceil();
- final galleryHeight = (thumbnailHeight * numberOfRows) +
- (galleryGridSpacing * (numberOfRows - 1));
- return galleryHeight + 120;
- }
- }
|