detail_page.dart 8.6 KB

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