detail_page.dart 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import 'package:extended_image/extended_image.dart';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photos/models/file.dart';
  7. import 'package:photos/models/file_type.dart';
  8. import 'package:photos/ui/fading_app_bar.dart';
  9. import 'package:photos/ui/fading_bottom_bar.dart';
  10. import 'package:photos/ui/gallery.dart';
  11. import 'package:photos/ui/image_editor_page.dart';
  12. import 'package:photos/ui/video_widget.dart';
  13. import 'package:photos/ui/zoomable_image.dart';
  14. import 'package:photos/utils/dialog_util.dart';
  15. import 'package:photos/utils/file_util.dart';
  16. import 'package:photos/utils/navigation_util.dart';
  17. class DetailPageConfiguration {
  18. final List<File> files;
  19. final GalleryLoader asyncLoader;
  20. final int selectedIndex;
  21. final String tagPrefix;
  22. DetailPageConfiguration(
  23. this.files,
  24. this.asyncLoader,
  25. this.selectedIndex,
  26. this.tagPrefix,
  27. );
  28. DetailPageConfiguration copyWith({
  29. List<File> files,
  30. GalleryLoader asyncLoader,
  31. int selectedIndex,
  32. String tagPrefix,
  33. }) {
  34. return DetailPageConfiguration(
  35. files ?? this.files,
  36. asyncLoader ?? this.asyncLoader,
  37. selectedIndex ?? this.selectedIndex,
  38. tagPrefix ?? this.tagPrefix,
  39. );
  40. }
  41. }
  42. class DetailPage extends StatefulWidget {
  43. final DetailPageConfiguration config;
  44. DetailPage(this.config, {key}) : super(key: key);
  45. @override
  46. _DetailPageState createState() => _DetailPageState();
  47. }
  48. class _DetailPageState extends State<DetailPage> {
  49. static const kLoadLimit = 100;
  50. final _logger = Logger("DetailPageState");
  51. bool _shouldDisableScroll = false;
  52. List<File> _files;
  53. PageController _pageController;
  54. int _selectedIndex = 0;
  55. bool _hasPageChanged = false;
  56. bool _hasLoadedTillStart = false;
  57. bool _hasLoadedTillEnd = false;
  58. bool _shouldHideAppBar = false;
  59. GlobalKey<FadingAppBarState> _appBarKey;
  60. GlobalKey<FadingBottomBarState> _bottomBarKey;
  61. @override
  62. void initState() {
  63. _files = widget.config.files;
  64. _selectedIndex = widget.config.selectedIndex;
  65. _preloadEntries(_selectedIndex);
  66. super.initState();
  67. }
  68. @override
  69. void dispose() {
  70. SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
  71. super.dispose();
  72. }
  73. @override
  74. Widget build(BuildContext context) {
  75. _logger.info("Opening " +
  76. _files[_selectedIndex].toString() +
  77. ". " +
  78. (_selectedIndex + 1).toString() +
  79. " / " +
  80. _files.length.toString() +
  81. " files .");
  82. _appBarKey = GlobalKey<FadingAppBarState>();
  83. _bottomBarKey = GlobalKey<FadingBottomBarState>();
  84. return Scaffold(
  85. appBar: FadingAppBar(
  86. _files[_selectedIndex],
  87. _onFileDeleted,
  88. 100,
  89. key: _appBarKey,
  90. ),
  91. extendBodyBehindAppBar: true,
  92. body: Center(
  93. child: Container(
  94. child: Stack(
  95. children: [
  96. _buildPageView(),
  97. FadingBottomBar(
  98. _files[_selectedIndex],
  99. _onEditFileRequested,
  100. key: _bottomBarKey,
  101. ),
  102. ],
  103. ),
  104. ),
  105. ),
  106. backgroundColor: Colors.black,
  107. );
  108. }
  109. Widget _buildPageView() {
  110. _logger.info("Building with " + _selectedIndex.toString());
  111. _pageController = PageController(initialPage: _selectedIndex);
  112. return PageView.builder(
  113. itemBuilder: (context, index) {
  114. final file = _files[index];
  115. Widget content;
  116. if (file.fileType == FileType.image) {
  117. content = ZoomableImage(
  118. file,
  119. shouldDisableScroll: (value) {
  120. setState(() {
  121. _shouldDisableScroll = value;
  122. });
  123. },
  124. tagPrefix: widget.config.tagPrefix,
  125. );
  126. } else if (file.fileType == FileType.video) {
  127. content = VideoWidget(
  128. file,
  129. autoPlay: !_hasPageChanged, // Autoplay if it was opened directly
  130. tagPrefix: widget.config.tagPrefix,
  131. );
  132. } else {
  133. content = Icon(Icons.error);
  134. }
  135. _preloadFiles(index);
  136. return GestureDetector(
  137. onTap: () {
  138. _shouldHideAppBar = !_shouldHideAppBar;
  139. if (_shouldHideAppBar) {
  140. _appBarKey.currentState.hide();
  141. _bottomBarKey.currentState.hide();
  142. } else {
  143. _appBarKey.currentState.show();
  144. _bottomBarKey.currentState.show();
  145. }
  146. Future.delayed(Duration.zero, () {
  147. SystemChrome.setEnabledSystemUIOverlays(
  148. _shouldHideAppBar ? [] : SystemUiOverlay.values,
  149. );
  150. });
  151. },
  152. child: content,
  153. );
  154. },
  155. onPageChanged: (index) {
  156. setState(() {
  157. _selectedIndex = index;
  158. _hasPageChanged = true;
  159. });
  160. _preloadEntries(index);
  161. _preloadFiles(index);
  162. },
  163. physics: _shouldDisableScroll
  164. ? NeverScrollableScrollPhysics()
  165. : PageScrollPhysics(),
  166. controller: _pageController,
  167. itemCount: _files.length,
  168. );
  169. }
  170. void _preloadEntries(int index) async {
  171. if (index == 0 && !_hasLoadedTillStart) {
  172. final result = await widget.config.asyncLoader(
  173. _files[index].creationTime + 1, DateTime.now().microsecondsSinceEpoch,
  174. limit: kLoadLimit, asc: true);
  175. setState(() {
  176. final files = result.files.reversed.toList();
  177. if (!result.hasMore) {
  178. _hasLoadedTillStart = true;
  179. }
  180. final length = files.length;
  181. files.addAll(_files);
  182. _files = files;
  183. _pageController.jumpToPage(length);
  184. _selectedIndex = length;
  185. });
  186. }
  187. if (index == _files.length - 1 && !_hasLoadedTillEnd) {
  188. final result = await widget.config
  189. .asyncLoader(0, _files[index].creationTime - 1, limit: kLoadLimit);
  190. setState(() {
  191. if (!result.hasMore) {
  192. _hasLoadedTillEnd = true;
  193. }
  194. _files.addAll(result.files);
  195. });
  196. }
  197. }
  198. void _preloadFiles(int index) {
  199. if (index > 0) {
  200. preloadFile(_files[index - 1]);
  201. }
  202. if (index < _files.length - 1) {
  203. preloadFile(_files[index + 1]);
  204. }
  205. }
  206. Future<void> _onFileDeleted(File file) async {
  207. final totalFiles = _files.length;
  208. if (totalFiles == 1) {
  209. // Deleted the only file
  210. Navigator.of(context, rootNavigator: true).pop(); // Close pageview
  211. Navigator.of(context, rootNavigator: true).pop(); // Close gallery
  212. return;
  213. }
  214. if (_selectedIndex == totalFiles - 1) {
  215. // Deleted the last file
  216. await _pageController.previousPage(
  217. duration: Duration(milliseconds: 200), curve: Curves.easeInOut);
  218. setState(() {
  219. _files.remove(file);
  220. });
  221. } else {
  222. await _pageController.nextPage(
  223. duration: Duration(milliseconds: 200), curve: Curves.easeInOut);
  224. setState(() {
  225. _selectedIndex--;
  226. _files.remove(file);
  227. });
  228. }
  229. Navigator.of(context, rootNavigator: true).pop(); // Close dialog
  230. }
  231. Future<void> _onEditFileRequested(File file) async {
  232. final dialog = createProgressDialog(context, "please wait...");
  233. await dialog.show();
  234. final imageProvider =
  235. ExtendedFileImageProvider(await getFile(file), cacheRawData: true);
  236. await precacheImage(imageProvider, context);
  237. await dialog.hide();
  238. replacePage(
  239. context,
  240. ImageEditorPage(
  241. imageProvider,
  242. file,
  243. widget.config.copyWith(
  244. files: _files,
  245. selectedIndex: _selectedIndex,
  246. ),
  247. ),
  248. );
  249. }
  250. }