detail_page.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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/core/configuration.dart';
  7. import 'package:photos/core/constants.dart';
  8. import 'package:photos/core/errors.dart';
  9. import 'package:photos/models/file.dart';
  10. import 'package:photos/ui/fading_app_bar.dart';
  11. import 'package:photos/ui/fading_bottom_bar.dart';
  12. import 'package:photos/ui/file_widget.dart';
  13. import 'package:photos/ui/gallery.dart';
  14. import 'package:photos/ui/image_editor_page.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. enum DetailPageMode {
  19. minimalistic,
  20. full,
  21. }
  22. class DetailPageConfiguration {
  23. final List<File> files;
  24. final GalleryLoader asyncLoader;
  25. final int selectedIndex;
  26. final String tagPrefix;
  27. final DetailPageMode mode;
  28. DetailPageConfiguration(
  29. this.files,
  30. this.asyncLoader,
  31. this.selectedIndex,
  32. this.tagPrefix, {
  33. this.mode = DetailPageMode.full,
  34. });
  35. DetailPageConfiguration copyWith({
  36. List<File> files,
  37. GalleryLoader asyncLoader,
  38. int selectedIndex,
  39. String tagPrefix,
  40. }) {
  41. return DetailPageConfiguration(
  42. files ?? this.files,
  43. asyncLoader ?? this.asyncLoader,
  44. selectedIndex ?? this.selectedIndex,
  45. tagPrefix ?? this.tagPrefix,
  46. );
  47. }
  48. }
  49. class DetailPage extends StatefulWidget {
  50. final DetailPageConfiguration config;
  51. DetailPage(this.config, {key}) : super(key: key);
  52. @override
  53. _DetailPageState createState() => _DetailPageState();
  54. }
  55. class _DetailPageState extends State<DetailPage> {
  56. static const kLoadLimit = 100;
  57. final _logger = Logger("DetailPageState");
  58. bool _shouldDisableScroll = false;
  59. List<File> _files;
  60. PageController _pageController;
  61. int _selectedIndex = 0;
  62. bool _hasPageChanged = false;
  63. bool _hasLoadedTillStart = false;
  64. bool _hasLoadedTillEnd = false;
  65. bool _shouldHideAppBar = false;
  66. GlobalKey<FadingAppBarState> _appBarKey;
  67. GlobalKey<FadingBottomBarState> _bottomBarKey;
  68. @override
  69. void initState() {
  70. _files = [
  71. ...widget.config.files
  72. ]; // Make a copy since we append preceding and succeeding entries to this
  73. _selectedIndex = widget.config.selectedIndex;
  74. _preloadEntries();
  75. super.initState();
  76. }
  77. @override
  78. void dispose() {
  79. SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
  80. super.dispose();
  81. }
  82. @override
  83. Widget build(BuildContext context) {
  84. _logger.info("Opening " +
  85. _files[_selectedIndex].toString() +
  86. ". " +
  87. (_selectedIndex + 1).toString() +
  88. " / " +
  89. _files.length.toString() +
  90. " files .");
  91. _appBarKey = GlobalKey<FadingAppBarState>();
  92. _bottomBarKey = GlobalKey<FadingBottomBarState>();
  93. return Scaffold(
  94. appBar: FadingAppBar(
  95. _files[_selectedIndex],
  96. _onFileDeleted,
  97. Configuration.instance.getUserID(),
  98. 100,
  99. widget.config.mode == DetailPageMode.full,
  100. key: _appBarKey,
  101. ),
  102. extendBodyBehindAppBar: true,
  103. body: Center(
  104. child: Stack(
  105. children: [
  106. _buildPageView(),
  107. FadingBottomBar(
  108. _files[_selectedIndex],
  109. _onEditFileRequested,
  110. widget.config.mode == DetailPageMode.minimalistic,
  111. key: _bottomBarKey,
  112. ),
  113. ],
  114. ),
  115. ),
  116. backgroundColor: Colors.black,
  117. );
  118. }
  119. Widget _buildPageView() {
  120. _logger.info("Building with " + _selectedIndex.toString());
  121. _pageController = PageController(initialPage: _selectedIndex);
  122. return PageView.builder(
  123. itemBuilder: (context, index) {
  124. final file = _files[index];
  125. Widget content = FileWidget(
  126. file,
  127. autoPlay: !_hasPageChanged,
  128. tagPrefix: widget.config.tagPrefix,
  129. shouldDisableScroll: (value) {
  130. setState(() {
  131. _shouldDisableScroll = value;
  132. });
  133. },
  134. playbackCallback: (isPlaying) {
  135. _shouldHideAppBar = isPlaying;
  136. Future.delayed(Duration.zero, () {
  137. _toggleFullScreen();
  138. });
  139. },
  140. );
  141. _preloadFiles(index);
  142. return GestureDetector(
  143. onTap: () {
  144. _shouldHideAppBar = !_shouldHideAppBar;
  145. _toggleFullScreen();
  146. },
  147. child: content,
  148. );
  149. },
  150. onPageChanged: (index) {
  151. setState(() {
  152. _selectedIndex = index;
  153. _hasPageChanged = true;
  154. });
  155. _preloadEntries();
  156. _preloadFiles(index);
  157. },
  158. physics: _shouldDisableScroll
  159. ? NeverScrollableScrollPhysics()
  160. : PageScrollPhysics(),
  161. controller: _pageController,
  162. itemCount: _files.length,
  163. );
  164. }
  165. void _toggleFullScreen() {
  166. if (_shouldHideAppBar) {
  167. _appBarKey.currentState.hide();
  168. _bottomBarKey.currentState.hide();
  169. } else {
  170. _appBarKey.currentState.show();
  171. _bottomBarKey.currentState.show();
  172. }
  173. Future.delayed(Duration.zero, () {
  174. SystemChrome.setEnabledSystemUIOverlays(
  175. _shouldHideAppBar ? [] : SystemUiOverlay.values,
  176. );
  177. });
  178. }
  179. void _preloadEntries() async {
  180. if (widget.config.asyncLoader == null) {
  181. return;
  182. }
  183. if (_selectedIndex == 0 && !_hasLoadedTillStart) {
  184. final result = await widget.config.asyncLoader(
  185. _files[_selectedIndex].creationTime + 1,
  186. DateTime.now().microsecondsSinceEpoch,
  187. limit: kLoadLimit,
  188. asc: true);
  189. setState(() {
  190. // Returned result could be a subtype of File
  191. // ignore: unnecessary_cast
  192. final files = result.files.reversed.map((e) => e as File).toList();
  193. if (!result.hasMore) {
  194. _hasLoadedTillStart = true;
  195. }
  196. final length = files.length;
  197. files.addAll(_files);
  198. _files = files;
  199. _pageController.jumpToPage(length);
  200. _selectedIndex = length;
  201. });
  202. }
  203. if (_selectedIndex == _files.length - 1 && !_hasLoadedTillEnd) {
  204. final result = await widget.config.asyncLoader(
  205. kGalleryLoadStartTime, _files[_selectedIndex].creationTime - 1,
  206. limit: kLoadLimit);
  207. setState(() {
  208. if (!result.hasMore) {
  209. _hasLoadedTillEnd = true;
  210. }
  211. _files.addAll(result.files);
  212. });
  213. }
  214. }
  215. void _preloadFiles(int index) {
  216. if (index > 0) {
  217. preloadFile(_files[index - 1]);
  218. }
  219. if (index < _files.length - 1) {
  220. preloadFile(_files[index + 1]);
  221. }
  222. }
  223. Future<void> _onFileDeleted(File file) async {
  224. final totalFiles = _files.length;
  225. if (totalFiles == 1) {
  226. // Deleted the only file
  227. Navigator.of(context).pop(); // Close pageview
  228. return;
  229. }
  230. if (_selectedIndex == totalFiles - 1) {
  231. // Deleted the last file
  232. await _pageController.previousPage(
  233. duration: Duration(milliseconds: 200), curve: Curves.easeInOut);
  234. setState(() {
  235. _files.remove(file);
  236. });
  237. } else {
  238. await _pageController.nextPage(
  239. duration: Duration(milliseconds: 200), curve: Curves.easeInOut);
  240. setState(() {
  241. _selectedIndex--;
  242. _files.remove(file);
  243. });
  244. }
  245. }
  246. Future<void> _onEditFileRequested(File file) async {
  247. if (file.uploadedFileID != null &&
  248. file.ownerID != Configuration.instance.getUserID()) {
  249. _logger.severe("Attempt to edit unowned file", UnauthorizedEditError(),
  250. StackTrace.current);
  251. showErrorDialog(context, "sorry",
  252. "we don't support editing photos and albums that you don't own yet");
  253. return;
  254. }
  255. final dialog = createProgressDialog(context, "please wait...");
  256. await dialog.show();
  257. final imageProvider =
  258. ExtendedFileImageProvider(await getFile(file), cacheRawData: true);
  259. await precacheImage(imageProvider, context);
  260. await dialog.hide();
  261. replacePage(
  262. context,
  263. ImageEditorPage(
  264. imageProvider,
  265. file,
  266. widget.config.copyWith(
  267. files: _files,
  268. selectedIndex: _selectedIndex,
  269. ),
  270. ),
  271. );
  272. }
  273. }