import 'package:extended_image/extended_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; import 'package:photos/models/file.dart'; import 'package:photos/ui/fading_app_bar.dart'; import 'package:photos/ui/fading_bottom_bar.dart'; import 'package:photos/ui/file_widget.dart'; import 'package:photos/ui/gallery.dart'; import 'package:photos/ui/image_editor_page.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/file_util.dart'; import 'package:photos/utils/navigation_util.dart'; enum DetailPageMode { minimalistic, full, } class DetailPageConfiguration { final List files; final GalleryLoader asyncLoader; final int selectedIndex; final String tagPrefix; final DetailPageMode mode; DetailPageConfiguration( this.files, this.asyncLoader, this.selectedIndex, this.tagPrefix, { this.mode = DetailPageMode.full, }); DetailPageConfiguration copyWith({ List files, GalleryLoader asyncLoader, int selectedIndex, String tagPrefix, }) { return DetailPageConfiguration( files ?? this.files, asyncLoader ?? this.asyncLoader, selectedIndex ?? this.selectedIndex, tagPrefix ?? this.tagPrefix, ); } } class DetailPage extends StatefulWidget { final DetailPageConfiguration config; DetailPage(this.config, {key}) : super(key: key); @override _DetailPageState createState() => _DetailPageState(); } class _DetailPageState extends State { static const kLoadLimit = 100; final _logger = Logger("DetailPageState"); bool _shouldDisableScroll = false; List _files; PageController _pageController; int _selectedIndex = 0; bool _hasPageChanged = false; bool _hasLoadedTillStart = false; bool _hasLoadedTillEnd = false; bool _shouldHideAppBar = false; GlobalKey _appBarKey; GlobalKey _bottomBarKey; @override void initState() { _files = [ ...widget.config.files ]; // Make a copy since we append preceding and succeeding entries to this _selectedIndex = widget.config.selectedIndex; _preloadEntries(); super.initState(); } @override void dispose() { SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); super.dispose(); } @override Widget build(BuildContext context) { _logger.info("Opening " + _files[_selectedIndex].toString() + ". " + (_selectedIndex + 1).toString() + " / " + _files.length.toString() + " files ."); _appBarKey = GlobalKey(); _bottomBarKey = GlobalKey(); return Scaffold( appBar: FadingAppBar( _files[_selectedIndex], _onFileDeleted, Configuration.instance.getUserID(), 100, widget.config.mode == DetailPageMode.full, key: _appBarKey, ), extendBodyBehindAppBar: true, body: Center( child: Stack( children: [ _buildPageView(), FadingBottomBar( _files[_selectedIndex], _onEditFileRequested, widget.config.mode == DetailPageMode.minimalistic, key: _bottomBarKey, ), ], ), ), backgroundColor: Colors.black, ); } Widget _buildPageView() { _logger.info("Building with " + _selectedIndex.toString()); _pageController = PageController(initialPage: _selectedIndex); return PageView.builder( itemBuilder: (context, index) { final file = _files[index]; Widget content = FileWidget( file, autoPlay: !_hasPageChanged, tagPrefix: widget.config.tagPrefix, shouldDisableScroll: (value) { setState(() { _shouldDisableScroll = value; }); }, playbackCallback: (isPlaying) { _shouldHideAppBar = isPlaying; Future.delayed(Duration.zero, () { _toggleFullScreen(); }); }, ); _preloadFiles(index); return GestureDetector( onTap: () { _shouldHideAppBar = !_shouldHideAppBar; _toggleFullScreen(); }, child: content, ); }, onPageChanged: (index) { setState(() { _selectedIndex = index; _hasPageChanged = true; }); _preloadEntries(); _preloadFiles(index); }, physics: _shouldDisableScroll ? NeverScrollableScrollPhysics() : PageScrollPhysics(), controller: _pageController, itemCount: _files.length, ); } void _toggleFullScreen() { if (_shouldHideAppBar) { _appBarKey.currentState.hide(); _bottomBarKey.currentState.hide(); } else { _appBarKey.currentState.show(); _bottomBarKey.currentState.show(); } Future.delayed(Duration.zero, () { SystemChrome.setEnabledSystemUIOverlays( _shouldHideAppBar ? [] : SystemUiOverlay.values, ); }); } void _preloadEntries() async { if (widget.config.asyncLoader == null) { return; } if (_selectedIndex == 0 && !_hasLoadedTillStart) { final result = await widget.config.asyncLoader( _files[_selectedIndex].creationTime + 1, DateTime.now().microsecondsSinceEpoch, limit: kLoadLimit, asc: true); setState(() { // Returned result could be a subtype of File // ignore: unnecessary_cast final files = result.files.reversed.map((e) => e as File).toList(); if (!result.hasMore) { _hasLoadedTillStart = true; } final length = files.length; files.addAll(_files); _files = files; _pageController.jumpToPage(length); _selectedIndex = length; }); } if (_selectedIndex == _files.length - 1 && !_hasLoadedTillEnd) { final result = await widget.config.asyncLoader( kGalleryLoadStartTime, _files[_selectedIndex].creationTime - 1, limit: kLoadLimit); setState(() { if (!result.hasMore) { _hasLoadedTillEnd = true; } _files.addAll(result.files); }); } } void _preloadFiles(int index) { if (index > 0) { preloadFile(_files[index - 1]); } if (index < _files.length - 1) { preloadFile(_files[index + 1]); } } Future _onFileDeleted(File file) async { final totalFiles = _files.length; if (totalFiles == 1) { // Deleted the only file Navigator.of(context, rootNavigator: true).pop(); // Close pageview Navigator.of(context, rootNavigator: true).pop(); // Close gallery return; } if (_selectedIndex == totalFiles - 1) { // Deleted the last file await _pageController.previousPage( duration: Duration(milliseconds: 200), curve: Curves.easeInOut); setState(() { _files.remove(file); }); } else { await _pageController.nextPage( duration: Duration(milliseconds: 200), curve: Curves.easeInOut); setState(() { _selectedIndex--; _files.remove(file); }); } Navigator.of(context, rootNavigator: true).pop(); // Close dialog } Future _onEditFileRequested(File file) async { if (file.uploadedFileID != null && file.ownerID != Configuration.instance.getUserID()) { _logger.severe("Attempt to edit unowned file", UnauthorizedEditError(), StackTrace.current); showErrorDialog(context, "sorry", "we don't support editing photos and albums that you don't own yet"); return; } final dialog = createProgressDialog(context, "please wait..."); await dialog.show(); final imageProvider = ExtendedFileImageProvider(await getFile(file), cacheRawData: true); await precacheImage(imageProvider, context); await dialog.hide(); replacePage( context, ImageEditorPage( imageProvider, file, widget.config.copyWith( files: _files, selectedIndex: _selectedIndex, ), ), ); } }