ente/lib/ui/home/memories/full_screen_memory.dart
2024-01-05 16:26:38 +05:30

434 lines
13 KiB
Dart

import "dart:async";
import "dart:io";
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/memory.dart";
import "package:photos/services/memories_service.dart";
import "package:photos/theme/text_style.dart";
import "package:photos/ui/actions/file/file_actions.dart";
import "package:photos/ui/viewer/file/file_widget.dart";
import "package:photos/ui/viewer/file_details/favorite_widget.dart";
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 int initialIndex;
const FullScreenMemory(
this.title,
this.initialIndex, {
super.key,
});
@override
State<FullScreenMemory> createState() => _FullScreenMemoryState();
}
class _FullScreenMemoryState extends State<FullScreenMemory> {
PageController? _pageController;
final _showTitle = ValueNotifier<bool>(true);
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_showTitle.value = false;
});
}
});
}
@override
void dispose() {
_pageController?.dispose();
_showTitle.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final inheritedData = FullScreenMemoryData.of(context)!;
final showStepProgressIndicator = inheritedData.memories.length < 60;
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
toolbarHeight: 84,
automaticallyImplyLeading: false,
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
),
),
),
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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,
),
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
),
],
),
],
);
},
),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.6),
Colors.black.withOpacity(0.5),
Colors.transparent,
],
stops: const [0, 0.6, 1],
),
),
),
backgroundColor: const Color(0x00000000),
elevation: 0,
),
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
Widget build(BuildContext context) {
final inheritedData = FullScreenMemoryData.of(context)!;
final currentFile = inheritedData.memories[pageViewIndex].file;
final List<Widget> rowChildren = [
IconButton(
icon: Icon(
Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
color: Colors.white, //same for both themes
),
onPressed: () {
showDetailsSheet(context, 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(
Icons.adaptive.share,
color: Colors.white, //same for both themes
),
onPressed: () {
share(context, [currentFile]);
},
),
);
return SafeArea(
top: false,
child: Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: rowChildren,
),
),
);
}
}
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,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.5), //same for both themes
Colors.transparent,
],
stops: const [0, 0.8],
),
),
),
);
}
}