Merge branch 'main' into isar

This commit is contained in:
Vishnu Mohandas 2024-01-06 20:30:56 +05:30 committed by GitHub
commit 0fad6423b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 343 additions and 307 deletions

View file

@ -5,7 +5,6 @@ import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:intl/intl.dart";
import "package:photos/core/configuration.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/models/memory.dart";
import "package:photos/services/memories_service.dart";
import "package:photos/theme/text_style.dart";
@ -16,98 +15,194 @@ import "package:photos/utils/file_util.dart";
import "package:photos/utils/share_util.dart";
import "package:step_progress_indicator/step_progress_indicator.dart";
//There are two states of variables that FullScreenMemory depends on:
//1. The list of memories
//2. The current index of the page view
//1
//Only when items are deleted will list of memories change and this requires the
//whole screen to be rebuild. So the InheritedWidget is updated using the Updater
//widget which will then lead to a rebuild of all widgets that call
//InheritedWidget.of(context).
//2
//There are widgets that doesn't come inside the PageView that needs to rebuild
//with new state when page index is changed. So the index is stored in a
//ValueNotifier inside the InheritedWidget and the widgets that need to change
//are wrapped in a ValueListenableBuilder.
class FullScreenMemoryDataUpdater extends StatefulWidget {
final List<Memory> memories;
final int initialIndex;
final Widget child;
const FullScreenMemoryDataUpdater({
required this.memories,
required this.initialIndex,
required this.child,
super.key,
});
@override
State<FullScreenMemoryDataUpdater> createState() =>
_FullScreenMemoryDataUpdaterState();
}
class _FullScreenMemoryDataUpdaterState
extends State<FullScreenMemoryDataUpdater> {
late ValueNotifier<int> indexNotifier;
@override
void initState() {
super.initState();
indexNotifier = ValueNotifier(widget.initialIndex);
MemoriesService.instance
.markMemoryAsSeen(widget.memories[widget.initialIndex]);
}
@override
void dispose() {
indexNotifier.dispose();
super.dispose();
}
void removeCurrentMemory() {
setState(() {
widget.memories.removeAt(indexNotifier.value);
});
}
@override
Widget build(BuildContext context) {
return FullScreenMemoryData(
memories: widget.memories,
indexNotifier: indexNotifier,
removeCurrentMemory: removeCurrentMemory,
child: widget.child,
);
}
}
class FullScreenMemoryData extends InheritedWidget {
final List<Memory> memories;
final ValueNotifier<int> indexNotifier;
final VoidCallback removeCurrentMemory;
const FullScreenMemoryData({
required this.memories,
required this.indexNotifier,
required this.removeCurrentMemory,
required Widget child,
Key? key,
}) : super(child: child, key: key);
static FullScreenMemoryData? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<FullScreenMemoryData>();
}
@override
bool updateShouldNotify(FullScreenMemoryData oldWidget) {
// Checking oldWidget.memories.length != memories.length here doesn't work
//because the old widget and new widget reference the same memories list.
return true;
}
}
class FullScreenMemory extends StatefulWidget {
final String title;
final List<Memory> memories;
final int index;
const FullScreenMemory(this.title, this.memories, this.index, {Key? key})
: super(key: key);
final int initialIndex;
const FullScreenMemory(
this.title,
this.initialIndex, {
super.key,
});
@override
State<FullScreenMemory> createState() => _FullScreenMemoryState();
}
class _FullScreenMemoryState extends State<FullScreenMemory> {
int _index = 0;
double _opacity = 1;
// shows memory counter as index+1/totalFiles for large number of memories
// when the top step indicator isn't visible.
bool _showCounter = false;
bool _showStepIndicator = true;
PageController? _pageController;
final bool _shouldDisableScroll = false;
late int currentUserID;
final GlobalKey shareButtonKey = GlobalKey();
final _showTitle = ValueNotifier<bool>(true);
@override
void initState() {
super.initState();
_index = widget.index;
currentUserID = Configuration.instance.getUserID() ?? 0;
_showStepIndicator = widget.memories.length <= 60;
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_opacity = 0;
_showCounter = !_showStepIndicator;
_showTitle.value = false;
});
}
});
MemoriesService.instance.markMemoryAsSeen(widget.memories[_index]);
}
@override
void dispose() {
_pageController?.dispose();
_showTitle.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final file = widget.memories[_index].file;
final inheritedData = FullScreenMemoryData.of(context)!;
final showStepProgressIndicator = inheritedData.memories.length < 60;
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
toolbarHeight: 84,
automaticallyImplyLeading: false,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_showStepIndicator
? StepProgressIndicator(
totalSteps: widget.memories.length,
currentStep: _index + 1,
size: 2,
selectedColor: Colors.white, //same for both themes
unselectedColor: Colors.white.withOpacity(0.4),
)
: const SizedBox.shrink(),
const SizedBox(
height: 18,
title: ValueListenableBuilder(
valueListenable: inheritedData.indexNotifier,
child: Padding(
padding: const EdgeInsets.only(right: 16),
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: const Icon(
Icons.close,
color: Colors.white, //same for both themes
),
),
Row(
),
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: const Icon(
Icons.close,
color: Colors.white, //same for both themes
),
),
showStepProgressIndicator
? StepProgressIndicator(
totalSteps: inheritedData.memories.length,
currentStep: value + 1,
size: 2,
selectedColor: Colors.white, //same for both themes
unselectedColor: Colors.white.withOpacity(0.4),
)
: const SizedBox.shrink(),
const SizedBox(
height: 18,
),
Text(
DateFormat.yMMMd(Localizations.localeOf(context).languageCode)
.format(
DateTime.fromMicrosecondsSinceEpoch(
file.creationTime!,
Row(
children: [
child!,
Text(
DateFormat.yMMMd(
Localizations.localeOf(context).languageCode,
).format(
DateTime.fromMicrosecondsSinceEpoch(
inheritedData.memories[value].file.creationTime!,
),
),
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Colors.white,
), //same for both themes
),
),
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Colors.white,
), //same for both themes
],
),
],
),
],
);
},
),
flexibleSpace: Container(
decoration: BoxDecoration(
@ -126,86 +221,108 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
backgroundColor: const Color(0x00000000),
elevation: 0,
),
extendBodyBehindAppBar: true,
body: Container(
key: ValueKey(widget.memories.length),
color: Colors.black,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
_buildSwiper(),
bottomGradient(),
_buildInfoText(),
_buildBottomIcons(),
],
),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
PageView.builder(
controller: _pageController ??= PageController(
initialPage: widget.initialIndex,
),
itemBuilder: (context, index) {
if (index < inheritedData.memories.length - 1) {
final nextFile = inheritedData.memories[index + 1].file;
preloadThumbnail(nextFile);
preloadFile(nextFile);
}
return Stack(
alignment: Alignment.bottomCenter,
children: [
GestureDetector(
onTapDown: (TapDownDetails details) {
final screenWidth = MediaQuery.of(context).size.width;
final edgeWidth = screenWidth * 0.20;
if (details.localPosition.dx < edgeWidth) {
if (index > 0) {
_pageController!.previousPage(
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
} else if (details.localPosition.dx >
screenWidth - edgeWidth) {
if (index < (inheritedData.memories.length - 1)) {
_pageController!.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
}
},
child: FileWidget(
inheritedData.memories[index].file,
autoPlay: false,
tagPrefix: "memories",
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
),
),
const BottomGradient(),
BottomIcons(index),
],
);
},
onPageChanged: (index) {
unawaited(
MemoriesService.instance
.markMemoryAsSeen(inheritedData.memories[index]),
);
inheritedData.indexNotifier.value = index;
},
itemCount: inheritedData.memories.length,
),
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 72),
child: ValueListenableBuilder(
builder: (context, value, _) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: value
? Hero(
tag: widget.title,
child: Text(
widget.title,
style: darkTextTheme.h2,
),
)
: showStepProgressIndicator
? const SizedBox.shrink()
: const MemoryCounter(),
);
},
valueListenable: _showTitle,
),
),
),
],
),
);
}
}
class BottomIcons extends StatelessWidget {
final int pageViewIndex;
const BottomIcons(this.pageViewIndex, {super.key});
@override
void dispose() {
debugPrint("FullScreenMemoryDisposed");
// _pageController?.dispose();
_pageController = null;
super.dispose();
}
Widget build(BuildContext context) {
final inheritedData = FullScreenMemoryData.of(context)!;
final currentFile = inheritedData.memories[pageViewIndex].file;
Future<void> onFileDeleted(Memory removedMemory) async {
if (!mounted) {
return;
}
final totalFiles = widget.memories.length;
if (totalFiles == 1) {
// Deleted the only file
Navigator.of(context).pop(); // Close pageview
return;
}
if (_index == totalFiles - 1) {
// Deleted the last file
widget.memories.remove(removedMemory);
await _pageController!.previousPage(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
setState(() {});
} else {
widget.memories.remove(removedMemory);
await _pageController!.nextPage(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
setState(() {});
}
}
Hero _buildInfoText() {
return Hero(
tag: widget.title,
child: SafeArea(
child: Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.fromLTRB(0, 0, 0, 72),
child: _showCounter
? Text(
'${_index + 1}/${widget.memories.length}',
style: darkTextTheme.bodyMuted,
)
: AnimatedOpacity(
opacity: _opacity,
duration: const Duration(milliseconds: 500),
child: Text(
widget.title,
style: darkTextTheme.h2,
),
),
),
),
);
}
Widget _buildBottomIcons() {
final EnteFile currentFile = widget.memories[_index].file;
final List<Widget> rowChildren = [
IconButton(
icon: Icon(
@ -217,31 +334,35 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
},
),
];
if (currentFile.ownerID == null || currentUserID == currentFile.ownerID) {
rowChildren.addAll(
[
IconButton(
icon: Icon(
Platform.isAndroid ? Icons.delete_outline : CupertinoIcons.delete,
color: Colors.white, //same for both themes
),
onPressed: () async {
await showSingleFileDeleteSheet(
context,
currentFile,
onFileRemoved: (file) =>
{onFileDeleted(widget.memories[_index])},
);
},
),
SizedBox(
height: 32,
child: FavoriteWidget(currentFile),
),
],
);
}
if (currentFile.ownerID == null ||
(Configuration.instance.getUserID() ?? 0) == currentFile.ownerID) {
rowChildren.addAll([
IconButton(
icon: Icon(
Platform.isAndroid ? Icons.delete_outline : CupertinoIcons.delete,
color: Colors.white, //same for both themes
),
onPressed: () async {
await showSingleFileDeleteSheet(
context,
inheritedData.memories[inheritedData.indexNotifier.value].file,
onFileRemoved: (file) => {
inheritedData.removeCurrentMemory.call(),
if (inheritedData.memories.isEmpty)
{
Navigator.of(context).pop(),
},
},
);
},
),
SizedBox(
height: 32,
child: FavoriteWidget(currentFile),
),
]);
}
rowChildren.add(
IconButton(
icon: Icon(
@ -255,6 +376,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
);
return SafeArea(
top: false,
child: Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 20),
@ -265,8 +387,31 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
),
);
}
}
Widget bottomGradient() {
class MemoryCounter extends StatelessWidget {
const MemoryCounter({super.key});
@override
Widget build(BuildContext context) {
final inheritedData = FullScreenMemoryData.of(context)!;
return ValueListenableBuilder(
valueListenable: inheritedData.indexNotifier,
builder: (context, value, _) {
return Text(
"${value + 1}/${inheritedData.memories.length}",
style: darkTextTheme.bodyMuted,
);
},
);
}
}
class BottomGradient extends StatelessWidget {
const BottomGradient({super.key});
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
height: 124,
@ -285,72 +430,4 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
),
);
}
Widget _buildSwiper() {
debugPrint(
"FullScreenbuildSwiper: $_index and total ${widget.memories.length}",
);
_pageController ??= PageController(initialPage: _index);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (TapDownDetails details) {
if (_shouldDisableScroll) {
return;
}
final screenWidth = MediaQuery.of(context).size.width;
final edgeWidth = screenWidth * 0.20; // 20% of screen width
if (details.localPosition.dx < edgeWidth) {
if (_index > 0) {
_pageController!.previousPage(
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
} else if (details.localPosition.dx > screenWidth - edgeWidth) {
if (_index < (widget.memories.length - 1)) {
_pageController!.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
}
}
},
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
if (index < widget.memories.length - 1) {
final nextFile = widget.memories[index + 1].file;
preloadThumbnail(nextFile);
preloadFile(nextFile);
}
final file = widget.memories[index].file;
return FileWidget(
file,
autoPlay: false,
tagPrefix: "memories",
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
);
},
itemCount: widget.memories.length,
controller: _pageController!,
onPageChanged: (index) async {
unawaited(
MemoriesService.instance.markMemoryAsSeen(widget.memories[index]),
);
if (mounted) {
debugPrint(
"FullScreenonPageChanged: $index and total ${widget.memories.length}",
);
setState(() {
_index = index;
});
}
},
physics: _shouldDisableScroll
? const NeverScrollableScrollPhysics()
: const PageScrollPhysics(),
),
);
}
}

View file

@ -64,6 +64,7 @@ class _MemoriesWidgetState extends State<MemoriesWidget> {
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: memoryWidgets,

View file

@ -2,7 +2,7 @@ import "package:flutter/material.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/memory.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/home/memories/full_screen_memory.dart";
import 'package:photos/ui/home/memories/full_screen_memory.dart';
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/utils/navigation_util.dart";
@ -21,13 +21,22 @@ class MemoryCovertWidget extends StatefulWidget {
class _MemoryCovertWidgetState extends State<MemoryCovertWidget> {
@override
Widget build(BuildContext context) {
//memories will be empty if all memories are deleted and setState is called
//after FullScreenMemory screen is popped
if (widget.memories.isEmpty) {
return const SizedBox.shrink();
}
final index = _getNextMemoryIndex();
final title = _getTitle(widget.memories[index]);
return GestureDetector(
onTap: () async {
await routeToPage(
context,
FullScreenMemory(title, widget.memories, index),
FullScreenMemoryDataUpdater(
initialIndex: index,
memories: widget.memories,
child: FullScreenMemory(title, index),
),
forceCustomPageRoute: true,
);
setState(() {});

View file

@ -198,81 +198,6 @@ class SearchWidgetState extends State<SearchWidget> {
);
}
Future<List<SearchResult>> getSearchResultsForQuery(
BuildContext context,
String query,
) async {
final Completer<List<SearchResult>> completer = Completer();
_debouncer.run(
() {
return _getSearchResultsFromService(context, query, completer);
},
);
return completer.future;
}
Future<void> _getSearchResultsFromService(
BuildContext context,
String query,
Completer completer,
) async {
final List<SearchResult> allResults = [];
if (query.isEmpty) {
completer.complete(allResults);
return;
}
try {
if (_isYearValid(query)) {
final yearResults = await _searchService.getYearSearchResults(query);
allResults.addAll(yearResults);
}
final holidayResults =
await _searchService.getHolidaySearchResults(context, query);
allResults.addAll(holidayResults);
final fileTypeSearchResults =
await _searchService.getFileTypeResults(context, query);
allResults.addAll(fileTypeSearchResults);
final captionAndDisplayNameResult =
await _searchService.getCaptionAndNameResults(query);
allResults.addAll(captionAndDisplayNameResult);
final fileExtnResult =
await _searchService.getFileExtensionResults(query);
allResults.addAll(fileExtnResult);
final locationResult = await _searchService.getLocationResults(query);
allResults.addAll(locationResult);
final collectionResults =
await _searchService.getCollectionSearchResults(query);
allResults.addAll(collectionResults);
final monthResults =
await _searchService.getMonthSearchResults(context, query);
allResults.addAll(monthResults);
final possibleEvents =
await _searchService.getDateResults(context, query);
allResults.addAll(possibleEvents);
final magicResults =
await _searchService.getMagicSearchResults(context, query);
allResults.addAll(magicResults);
final contactResults =
await _searchService.getContactSearchResults(query);
allResults.addAll(contactResults);
} catch (e, s) {
_logger.severe("error during search", e, s);
}
completer.complete(allResults);
}
Stream<List<SearchResult>> _getSearchResultsStream(
BuildContext context,
String query,

View file

@ -119,9 +119,33 @@ Future<List<EnteFile>> convertIncomingSharedMediaToFile(
// fileName: img_x.jpg
enteFile.title = basename(media.path);
var ioFile = File(media.path);
ioFile = ioFile.renameSync(
Configuration.instance.getSharedMediaDirectory() + "/" + enteFile.title!,
);
try {
ioFile = ioFile.renameSync(
Configuration.instance.getSharedMediaDirectory() +
"/" +
enteFile.title!,
);
} catch (e) {
if (e is FileSystemException) {
//from renameSync docs:
//On some platforms, a rename operation cannot move a file between
//different file systems. If that is the case, instead copySync the
//file to the new location and then deleteSync the original.
_logger.info("Creating new copy of file in path ${ioFile.path}");
final newIoFile = ioFile.copySync(
Configuration.instance.getSharedMediaDirectory() +
"/" +
enteFile.title!,
);
if (media.path.contains("io.ente.photos")) {
_logger.info("delete original file in path ${ioFile.path}");
ioFile.deleteSync();
}
ioFile = newIoFile;
} else {
rethrow;
}
}
enteFile.localID = sharedMediaIdentifier + enteFile.title!;
enteFile.collectionID = collectionID;
enteFile.fileType =