detail_page.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import 'package:extended_image/extended_image.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/core/constants.dart';
  7. import 'package:photos/core/errors.dart';
  8. import "package:photos/generated/l10n.dart";
  9. import 'package:photos/models/file.dart';
  10. import 'package:photos/ui/tools/editor/image_editor_page.dart';
  11. import "package:photos/ui/viewer/file/file_app_bar.dart";
  12. import "package:photos/ui/viewer/file/file_bottom_bar.dart";
  13. import 'package:photos/ui/viewer/file/file_widget.dart';
  14. import 'package:photos/ui/viewer/gallery/gallery.dart';
  15. import 'package:photos/utils/dialog_util.dart';
  16. import 'package:photos/utils/file_util.dart';
  17. import 'package:photos/utils/navigation_util.dart';
  18. import 'package:photos/utils/toast_util.dart';
  19. enum DetailPageMode {
  20. minimalistic,
  21. full,
  22. }
  23. class DetailPageConfiguration {
  24. final List<EnteFile> files;
  25. final GalleryLoader? asyncLoader;
  26. final int selectedIndex;
  27. final String tagPrefix;
  28. final DetailPageMode mode;
  29. final bool sortOrderAsc;
  30. DetailPageConfiguration(
  31. this.files,
  32. this.asyncLoader,
  33. this.selectedIndex,
  34. this.tagPrefix, {
  35. this.mode = DetailPageMode.full,
  36. this.sortOrderAsc = false,
  37. });
  38. DetailPageConfiguration copyWith({
  39. List<EnteFile>? files,
  40. GalleryLoader? asyncLoader,
  41. int? selectedIndex,
  42. String? tagPrefix,
  43. bool? sortOrderAsc,
  44. }) {
  45. return DetailPageConfiguration(
  46. files ?? this.files,
  47. asyncLoader ?? this.asyncLoader,
  48. selectedIndex ?? this.selectedIndex,
  49. tagPrefix ?? this.tagPrefix,
  50. sortOrderAsc: sortOrderAsc ?? this.sortOrderAsc,
  51. );
  52. }
  53. }
  54. class DetailPage extends StatefulWidget {
  55. final DetailPageConfiguration config;
  56. const DetailPage(this.config, {key}) : super(key: key);
  57. @override
  58. State<DetailPage> createState() => _DetailPageState();
  59. }
  60. class _DetailPageState extends State<DetailPage> {
  61. static const kLoadLimit = 100;
  62. final _logger = Logger("DetailPageState");
  63. bool _shouldDisableScroll = false;
  64. List<EnteFile>? _files;
  65. late PageController _pageController;
  66. final _selectedIndexNotifier = ValueNotifier(0);
  67. bool _hasLoadedTillStart = false;
  68. bool _hasLoadedTillEnd = false;
  69. final _enableFullScreenNotifier = ValueNotifier(false);
  70. bool _isFirstOpened = true;
  71. @override
  72. void initState() {
  73. super.initState();
  74. _files = [
  75. ...widget.config.files,
  76. ]; // Make a copy since we append preceding and succeeding entries to this
  77. _selectedIndexNotifier.value = widget.config.selectedIndex;
  78. _preloadEntries();
  79. _pageController = PageController(initialPage: _selectedIndexNotifier.value);
  80. }
  81. @override
  82. void dispose() {
  83. _pageController.dispose();
  84. _enableFullScreenNotifier.dispose();
  85. _selectedIndexNotifier.dispose();
  86. SystemChrome.setEnabledSystemUIMode(
  87. SystemUiMode.manual,
  88. overlays: SystemUiOverlay.values,
  89. );
  90. super.dispose();
  91. }
  92. @override
  93. Widget build(BuildContext context) {
  94. _logger.info(
  95. "Opening " +
  96. _files![_selectedIndexNotifier.value].toString() +
  97. ". " +
  98. (_selectedIndexNotifier.value + 1).toString() +
  99. " / " +
  100. _files!.length.toString() +
  101. " files .",
  102. );
  103. return Scaffold(
  104. appBar: PreferredSize(
  105. preferredSize: const Size.fromHeight(80),
  106. child: ValueListenableBuilder(
  107. builder: (BuildContext context, int selectedIndex, _) {
  108. return FileAppBar(
  109. _files![selectedIndex],
  110. _onFileRemoved,
  111. Configuration.instance.getUserID(),
  112. 100,
  113. widget.config.mode == DetailPageMode.full,
  114. enableFullScreenNotifier: _enableFullScreenNotifier,
  115. );
  116. },
  117. valueListenable: _selectedIndexNotifier,
  118. ),
  119. ),
  120. extendBodyBehindAppBar: true,
  121. resizeToAvoidBottomInset: false,
  122. body: Center(
  123. child: Stack(
  124. children: [
  125. _buildPageView(context),
  126. ValueListenableBuilder(
  127. builder: (BuildContext context, int selectedIndex, _) {
  128. return FileBottomBar(
  129. _files![_selectedIndexNotifier.value],
  130. _onEditFileRequested,
  131. widget.config.mode == DetailPageMode.minimalistic,
  132. onFileRemoved: _onFileRemoved,
  133. userID: Configuration.instance.getUserID(),
  134. enableFullScreenNotifier: _enableFullScreenNotifier,
  135. );
  136. },
  137. valueListenable: _selectedIndexNotifier,
  138. ),
  139. ],
  140. ),
  141. ),
  142. );
  143. }
  144. Widget _buildPageView(BuildContext context) {
  145. final bottomPadding = MediaQuery.of(context).padding.bottom;
  146. _logger.info("Building with " + _selectedIndexNotifier.value.toString());
  147. return PageView.builder(
  148. itemBuilder: (context, index) {
  149. final file = _files![index];
  150. _preloadFiles(index);
  151. return GestureDetector(
  152. onTap: () {
  153. _toggleFullScreen();
  154. },
  155. child: FileWidget(
  156. file,
  157. autoPlay: shouldAutoPlay(),
  158. tagPrefix: widget.config.tagPrefix,
  159. shouldDisableScroll: (value) {
  160. if (_shouldDisableScroll != value) {
  161. setState(() {
  162. _shouldDisableScroll = value;
  163. });
  164. }
  165. },
  166. //Noticed that when the video is seeked, the video pops and moves the
  167. //seek bar along with it and it happens when bottomPadding is 0. So we
  168. //don't toggle full screen for cases where this issue happens.
  169. playbackCallback: bottomPadding != 0
  170. ? (isPlaying) {
  171. Future.delayed(Duration.zero, () {
  172. _toggleFullScreen();
  173. });
  174. }
  175. : null,
  176. backgroundDecoration: const BoxDecoration(color: Colors.black),
  177. ),
  178. );
  179. },
  180. onPageChanged: (index) {
  181. _selectedIndexNotifier.value = index;
  182. _preloadEntries();
  183. },
  184. physics: _shouldDisableScroll
  185. ? const NeverScrollableScrollPhysics()
  186. : const PageScrollPhysics(),
  187. controller: _pageController,
  188. itemCount: _files!.length,
  189. );
  190. }
  191. bool shouldAutoPlay() {
  192. if (_isFirstOpened) {
  193. _isFirstOpened = false;
  194. return true;
  195. }
  196. return false;
  197. }
  198. void _toggleFullScreen() {
  199. _enableFullScreenNotifier.value = !_enableFullScreenNotifier.value;
  200. Future.delayed(const Duration(milliseconds: 125), () {
  201. SystemChrome.setEnabledSystemUIMode(
  202. //to hide status bar?
  203. SystemUiMode.manual,
  204. overlays: _enableFullScreenNotifier.value ? [] : SystemUiOverlay.values,
  205. );
  206. });
  207. }
  208. Future<void> _preloadEntries() async {
  209. final isSortOrderAsc = widget.config.sortOrderAsc;
  210. if (widget.config.asyncLoader == null) return;
  211. if (_selectedIndexNotifier.value == 0 && !_hasLoadedTillStart) {
  212. await _loadStartEntries(isSortOrderAsc);
  213. }
  214. if (_selectedIndexNotifier.value == _files!.length - 1 &&
  215. !_hasLoadedTillEnd) {
  216. await _loadEndEntries(isSortOrderAsc);
  217. }
  218. }
  219. Future<void> _loadStartEntries(bool isSortOrderAsc) async {
  220. final result = isSortOrderAsc
  221. ? await widget.config.asyncLoader!(
  222. galleryLoadStartTime,
  223. _files![_selectedIndexNotifier.value].creationTime! - 1,
  224. limit: kLoadLimit,
  225. )
  226. : await widget.config.asyncLoader!(
  227. _files![_selectedIndexNotifier.value].creationTime! + 1,
  228. DateTime.now().microsecondsSinceEpoch,
  229. limit: kLoadLimit,
  230. asc: true,
  231. );
  232. setState(() {
  233. // Returned result could be a subtype of File
  234. // ignore: unnecessary_cast
  235. final files = result.files.reversed.map((e) => e as EnteFile).toList();
  236. if (!result.hasMore) {
  237. _hasLoadedTillStart = true;
  238. }
  239. final length = files.length;
  240. files.addAll(_files!);
  241. _files = files;
  242. _pageController.jumpToPage(length);
  243. _selectedIndexNotifier.value = length;
  244. });
  245. }
  246. Future<void> _loadEndEntries(bool isSortOrderAsc) async {
  247. final result = isSortOrderAsc
  248. ? await widget.config.asyncLoader!(
  249. _files![_selectedIndexNotifier.value].creationTime! + 1,
  250. DateTime.now().microsecondsSinceEpoch,
  251. limit: kLoadLimit,
  252. asc: true,
  253. )
  254. : await widget.config.asyncLoader!(
  255. galleryLoadStartTime,
  256. _files![_selectedIndexNotifier.value].creationTime! - 1,
  257. limit: kLoadLimit,
  258. );
  259. setState(() {
  260. if (!result.hasMore) {
  261. _hasLoadedTillEnd = true;
  262. }
  263. _files!.addAll(result.files);
  264. });
  265. }
  266. void _preloadFiles(int index) {
  267. if (index > 0) {
  268. preloadFile(_files![index - 1]);
  269. }
  270. if (index < _files!.length - 1) {
  271. preloadFile(_files![index + 1]);
  272. }
  273. }
  274. Future<void> _onFileRemoved(EnteFile file) async {
  275. final totalFiles = _files!.length;
  276. if (totalFiles == 1) {
  277. // Deleted the only file
  278. Navigator.of(context).pop(); // Close pageview
  279. return;
  280. }
  281. if (_selectedIndexNotifier.value == totalFiles - 1) {
  282. // Deleted the last file
  283. await _pageController.previousPage(
  284. duration: const Duration(milliseconds: 200),
  285. curve: Curves.easeInOut,
  286. );
  287. setState(() {
  288. _files!.remove(file);
  289. });
  290. } else {
  291. await _pageController.nextPage(
  292. duration: const Duration(milliseconds: 200),
  293. curve: Curves.easeInOut,
  294. );
  295. setState(() {
  296. _selectedIndexNotifier.value--;
  297. _files!.remove(file);
  298. });
  299. }
  300. }
  301. Future<void> _onEditFileRequested(EnteFile file) async {
  302. if (file.uploadedFileID != null &&
  303. file.ownerID != Configuration.instance.getUserID()) {
  304. _logger.severe(
  305. "Attempt to edit unowned file",
  306. UnauthorizedEditError(),
  307. StackTrace.current,
  308. );
  309. showErrorDialog(
  310. context,
  311. S.of(context).sorry,
  312. S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont,
  313. );
  314. return;
  315. }
  316. final dialog = createProgressDialog(context, S.of(context).pleaseWait);
  317. await dialog.show();
  318. try {
  319. final ioFile = await getFile(file);
  320. if (ioFile == null) {
  321. showShortToast(context, S.of(context).failedToFetchOriginalForEdit);
  322. await dialog.hide();
  323. return;
  324. }
  325. final imageProvider =
  326. ExtendedFileImageProvider(ioFile, cacheRawData: true);
  327. await precacheImage(imageProvider, context);
  328. await dialog.hide();
  329. replacePage(
  330. context,
  331. ImageEditorPage(
  332. imageProvider,
  333. file,
  334. widget.config.copyWith(
  335. files: _files,
  336. selectedIndex: _selectedIndexNotifier.value,
  337. ),
  338. ),
  339. );
  340. } catch (e) {
  341. await dialog.hide();
  342. _logger.warning("Failed to initiate edit", e);
  343. }
  344. }
  345. }