2021-08-24 14:56:14 +02:00
|
|
|
import 'dart:async';
|
2021-04-10 06:28:12 +02:00
|
|
|
import 'dart:math';
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
2022-01-25 11:17:19 +01:00
|
|
|
import 'package:kiwi/kiwi.dart';
|
2021-04-10 06:28:12 +02:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:nc_photos/account.dart';
|
2021-07-25 07:00:38 +02:00
|
|
|
import 'package:nc_photos/app_localizations.dart';
|
2022-01-25 11:17:19 +01:00
|
|
|
import 'package:nc_photos/di_container.dart';
|
2021-09-28 22:56:44 +02:00
|
|
|
import 'package:nc_photos/download_handler.dart';
|
2021-04-10 06:28:12 +02:00
|
|
|
import 'package:nc_photos/entity/album.dart';
|
|
|
|
import 'package:nc_photos/entity/file.dart';
|
2021-05-06 13:36:20 +02:00
|
|
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
2021-04-10 06:28:12 +02:00
|
|
|
import 'package:nc_photos/k.dart' as k;
|
2022-01-25 11:17:19 +01:00
|
|
|
import 'package:nc_photos/notified_action.dart';
|
2022-05-04 10:42:46 +02:00
|
|
|
import 'package:nc_photos/platform/features.dart' as features;
|
2021-08-23 16:19:33 +02:00
|
|
|
import 'package:nc_photos/pref.dart';
|
2021-07-10 17:06:04 +02:00
|
|
|
import 'package:nc_photos/share_handler.dart';
|
2021-04-10 06:28:12 +02:00
|
|
|
import 'package:nc_photos/theme.dart';
|
2022-01-25 11:17:19 +01:00
|
|
|
import 'package:nc_photos/use_case/update_property.dart';
|
2021-05-05 10:45:56 +02:00
|
|
|
import 'package:nc_photos/widget/animated_visibility.dart';
|
2021-08-23 19:28:25 +02:00
|
|
|
import 'package:nc_photos/widget/disposable.dart';
|
2022-05-04 10:42:46 +02:00
|
|
|
import 'package:nc_photos/widget/handler/enhance_handler.dart';
|
2021-12-02 11:47:37 +01:00
|
|
|
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
|
2021-07-31 17:38:26 +02:00
|
|
|
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
|
2022-07-12 22:11:27 +02:00
|
|
|
import 'package:nc_photos/widget/image_editor.dart';
|
2021-05-02 16:18:26 +02:00
|
|
|
import 'package:nc_photos/widget/image_viewer.dart';
|
2021-09-14 23:00:24 +02:00
|
|
|
import 'package:nc_photos/widget/slideshow_dialog.dart';
|
|
|
|
import 'package:nc_photos/widget/slideshow_viewer.dart';
|
2021-05-06 13:36:20 +02:00
|
|
|
import 'package:nc_photos/widget/video_viewer.dart';
|
2021-07-31 17:38:01 +02:00
|
|
|
import 'package:nc_photos/widget/viewer_bottom_app_bar.dart';
|
2021-04-10 06:28:12 +02:00
|
|
|
import 'package:nc_photos/widget/viewer_detail_pane.dart';
|
2021-09-14 23:00:24 +02:00
|
|
|
import 'package:nc_photos/widget/viewer_mixin.dart';
|
2021-04-10 06:28:12 +02:00
|
|
|
|
|
|
|
class ViewerArguments {
|
2021-08-13 22:18:35 +02:00
|
|
|
ViewerArguments(
|
|
|
|
this.account,
|
|
|
|
this.streamFiles,
|
|
|
|
this.startIndex, {
|
|
|
|
this.album,
|
|
|
|
});
|
2021-04-10 06:28:12 +02:00
|
|
|
|
|
|
|
final Account account;
|
|
|
|
final List<File> streamFiles;
|
|
|
|
final int startIndex;
|
2021-08-13 22:18:35 +02:00
|
|
|
final Album? album;
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class Viewer extends StatefulWidget {
|
|
|
|
static const routeName = "/viewer";
|
|
|
|
|
2021-07-23 22:05:57 +02:00
|
|
|
static Route buildRoute(ViewerArguments args) => MaterialPageRoute(
|
|
|
|
builder: (context) => Viewer.fromArgs(args),
|
|
|
|
);
|
|
|
|
|
2021-09-15 08:58:06 +02:00
|
|
|
const Viewer({
|
2021-07-23 22:05:57 +02:00
|
|
|
Key? key,
|
|
|
|
required this.account,
|
|
|
|
required this.streamFiles,
|
|
|
|
required this.startIndex,
|
2021-08-13 22:18:35 +02:00
|
|
|
this.album,
|
2021-04-10 06:28:12 +02:00
|
|
|
}) : super(key: key);
|
|
|
|
|
2021-07-23 22:05:57 +02:00
|
|
|
Viewer.fromArgs(ViewerArguments args, {Key? key})
|
2021-04-10 06:28:12 +02:00
|
|
|
: this(
|
|
|
|
key: key,
|
|
|
|
account: args.account,
|
|
|
|
streamFiles: args.streamFiles,
|
|
|
|
startIndex: args.startIndex,
|
2021-08-13 22:18:35 +02:00
|
|
|
album: args.album,
|
2021-04-10 06:28:12 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
createState() => _ViewerState();
|
|
|
|
|
|
|
|
final Account account;
|
|
|
|
final List<File> streamFiles;
|
|
|
|
final int startIndex;
|
2021-08-13 22:18:35 +02:00
|
|
|
|
|
|
|
/// The album these files belongs to, or null
|
|
|
|
final Album? album;
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
|
2021-09-14 23:00:24 +02:00
|
|
|
class _ViewerState extends State<Viewer>
|
|
|
|
with DisposableManagerMixin<Viewer>, ViewerControllersMixin<Viewer> {
|
2021-04-10 06:28:12 +02:00
|
|
|
@override
|
|
|
|
build(BuildContext context) {
|
|
|
|
return AppTheme(
|
|
|
|
child: Scaffold(
|
2021-04-21 07:22:10 +02:00
|
|
|
body: Builder(
|
2021-07-31 17:38:26 +02:00
|
|
|
builder: _buildContent,
|
|
|
|
),
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildContent(BuildContext context) {
|
2021-05-02 16:18:26 +02:00
|
|
|
return GestureDetector(
|
|
|
|
onTap: () {
|
|
|
|
setState(() {
|
|
|
|
_setShowActionBar(!_isShowAppBar);
|
|
|
|
});
|
2021-04-10 06:28:12 +02:00
|
|
|
},
|
2021-05-02 16:18:26 +02:00
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Container(color: Colors.black),
|
2021-07-31 17:38:26 +02:00
|
|
|
if (!_isViewerLoaded ||
|
2021-09-14 23:03:09 +02:00
|
|
|
_pageStates[_viewerController.currentPage]?.hasLoaded != true)
|
2021-09-15 08:58:06 +02:00
|
|
|
const Align(
|
2021-05-02 16:18:26 +02:00
|
|
|
alignment: Alignment.center,
|
2021-09-15 08:58:06 +02:00
|
|
|
child: CircularProgressIndicator(),
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
2021-07-31 17:38:26 +02:00
|
|
|
HorizontalPageViewer(
|
|
|
|
pageCount: widget.streamFiles.length,
|
|
|
|
pageBuilder: _buildPage,
|
|
|
|
initialPage: widget.startIndex,
|
|
|
|
controller: _viewerController,
|
|
|
|
viewportFraction: _viewportFraction,
|
|
|
|
canSwitchPage: _canSwitchPage(),
|
2021-08-05 12:38:43 +02:00
|
|
|
onPageChanged: (_) {
|
|
|
|
setState(() {});
|
|
|
|
},
|
2021-05-02 16:18:26 +02:00
|
|
|
),
|
|
|
|
_buildBottomAppBar(context),
|
|
|
|
_buildAppBar(context),
|
|
|
|
],
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildAppBar(BuildContext context) {
|
2022-01-25 11:17:19 +01:00
|
|
|
final index =
|
|
|
|
_isViewerLoaded ? _viewerController.currentPage : widget.startIndex;
|
|
|
|
final file = widget.streamFiles[index];
|
2021-04-10 06:28:12 +02:00
|
|
|
return Wrap(
|
|
|
|
children: [
|
2021-05-05 10:45:56 +02:00
|
|
|
AnimatedVisibility(
|
2021-04-10 06:28:12 +02:00
|
|
|
opacity: _isShowAppBar ? 1.0 : 0.0,
|
|
|
|
duration: k.animationDurationNormal,
|
2021-05-05 10:45:56 +02:00
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Container(
|
|
|
|
// + status bar height
|
|
|
|
height: kToolbarHeight + MediaQuery.of(context).padding.top,
|
2021-09-15 08:58:06 +02:00
|
|
|
decoration: const BoxDecoration(
|
2021-05-05 10:45:56 +02:00
|
|
|
gradient: LinearGradient(
|
2021-09-15 08:58:06 +02:00
|
|
|
begin: Alignment(0, -1),
|
|
|
|
end: Alignment(0, 1),
|
2021-05-05 10:45:56 +02:00
|
|
|
colors: [
|
|
|
|
Color.fromARGB(192, 0, 0, 0),
|
|
|
|
Color.fromARGB(0, 0, 0, 0),
|
|
|
|
],
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
),
|
2021-05-05 10:45:56 +02:00
|
|
|
),
|
|
|
|
AppBar(
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
shadowColor: Colors.transparent,
|
2021-09-15 12:39:43 +02:00
|
|
|
foregroundColor: Colors.white.withOpacity(.87),
|
2021-05-05 10:45:56 +02:00
|
|
|
actions: [
|
2022-01-25 11:17:19 +01:00
|
|
|
if (!_isDetailPaneActive && _canOpenDetailPane()) ...[
|
|
|
|
(_pageStates[index]?.favoriteOverride ?? file.isFavorite) ==
|
|
|
|
true
|
|
|
|
? IconButton(
|
|
|
|
icon: const Icon(Icons.star),
|
|
|
|
tooltip: L10n.global().unfavoriteTooltip,
|
|
|
|
onPressed: () => _onUnfavoritePressed(index),
|
|
|
|
)
|
|
|
|
: IconButton(
|
|
|
|
icon: const Icon(Icons.star_border),
|
|
|
|
tooltip: L10n.global().favoriteTooltip,
|
|
|
|
onPressed: () => _onFavoritePressed(index),
|
|
|
|
),
|
2021-05-05 10:45:56 +02:00
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.more_vert),
|
2021-08-29 13:51:43 +02:00
|
|
|
tooltip: L10n.global().detailsTooltip,
|
2021-05-05 10:45:56 +02:00
|
|
|
onPressed: _onDetailsPressed,
|
|
|
|
),
|
2022-01-25 11:17:19 +01:00
|
|
|
],
|
2021-05-05 10:45:56 +02:00
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildBottomAppBar(BuildContext context) {
|
2022-05-04 10:42:46 +02:00
|
|
|
final index =
|
|
|
|
_isViewerLoaded ? _viewerController.currentPage : widget.startIndex;
|
|
|
|
final file = widget.streamFiles[index];
|
2021-04-10 06:28:12 +02:00
|
|
|
return Align(
|
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
|
child: Material(
|
|
|
|
type: MaterialType.transparency,
|
2021-05-05 10:45:56 +02:00
|
|
|
child: AnimatedVisibility(
|
|
|
|
opacity: (_isShowAppBar && !_isDetailPaneActive) ? 1.0 : 0.0,
|
|
|
|
duration: !_isDetailPaneActive
|
|
|
|
? k.animationDurationNormal
|
|
|
|
: const Duration(milliseconds: 1),
|
2021-07-31 17:38:01 +02:00
|
|
|
child: ViewerBottomAppBar(
|
2021-08-13 19:30:47 +02:00
|
|
|
children: [
|
2021-10-09 10:42:18 +02:00
|
|
|
IconButton(
|
|
|
|
icon: Icon(
|
|
|
|
Icons.share_outlined,
|
|
|
|
color: Colors.white.withOpacity(.87),
|
2021-08-13 19:30:47 +02:00
|
|
|
),
|
2021-10-09 10:42:18 +02:00
|
|
|
tooltip: L10n.global().shareTooltip,
|
|
|
|
onPressed: () => _onSharePressed(context),
|
|
|
|
),
|
2022-05-04 10:42:46 +02:00
|
|
|
if (features.isSupportEnhancement &&
|
2022-07-12 22:11:27 +02:00
|
|
|
EnhanceHandler.isSupportedFormat(file)) ...[
|
|
|
|
IconButton(
|
|
|
|
icon: Icon(
|
|
|
|
Icons.tune_outlined,
|
|
|
|
color: Colors.white.withOpacity(.87),
|
|
|
|
),
|
|
|
|
tooltip: L10n.global().editTooltip,
|
|
|
|
onPressed: () => _onEditPressed(context),
|
|
|
|
),
|
2022-05-04 10:42:46 +02:00
|
|
|
IconButton(
|
|
|
|
icon: Icon(
|
|
|
|
Icons.auto_fix_high_outlined,
|
|
|
|
color: Colors.white.withOpacity(.87),
|
|
|
|
),
|
|
|
|
tooltip: L10n.global().enhanceTooltip,
|
|
|
|
onPressed: () => _onEnhancePressed(context),
|
|
|
|
),
|
2022-07-12 22:11:27 +02:00
|
|
|
],
|
2021-08-13 19:30:47 +02:00
|
|
|
IconButton(
|
|
|
|
icon: Icon(
|
|
|
|
Icons.download_outlined,
|
|
|
|
color: Colors.white.withOpacity(.87),
|
|
|
|
),
|
2021-08-29 13:51:43 +02:00
|
|
|
tooltip: L10n.global().downloadTooltip,
|
|
|
|
onPressed: _onDownloadPressed,
|
2021-08-13 19:30:47 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
icon: Icon(
|
|
|
|
Icons.delete_outlined,
|
|
|
|
color: Colors.white.withOpacity(.87),
|
|
|
|
),
|
2021-08-29 13:51:43 +02:00
|
|
|
tooltip: L10n.global().deleteTooltip,
|
2021-08-13 19:30:47 +02:00
|
|
|
onPressed: () => _onDeletePressed(context),
|
|
|
|
),
|
|
|
|
],
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildPage(BuildContext context, int index) {
|
|
|
|
if (_pageStates[index] == null) {
|
|
|
|
_onCreateNewPage(context, index);
|
2021-07-23 22:05:57 +02:00
|
|
|
} else if (!_pageStates[index]!.scrollController.hasClients) {
|
2021-04-10 06:28:12 +02:00
|
|
|
// the page has been moved out of view and is now coming back
|
|
|
|
_log.fine("[_buildPage] Recreating page#$index");
|
|
|
|
_onRecreatePageAfterMovedOut(context, index);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
_log.info("[_buildPage] $index");
|
|
|
|
}
|
|
|
|
|
|
|
|
return FractionallySizedBox(
|
2021-07-31 17:38:26 +02:00
|
|
|
widthFactor: 1 / _viewportFraction,
|
2021-04-10 06:28:12 +02:00
|
|
|
child: NotificationListener<ScrollNotification>(
|
|
|
|
onNotification: (notif) => _onPageContentScrolled(notif, index),
|
2022-01-21 19:50:33 +01:00
|
|
|
child: ScrollConfiguration(
|
|
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
controller: _pageStates[index]!.scrollController,
|
|
|
|
physics: !_isZoomed ? null : const NeverScrollableScrollPhysics(),
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
_buildItemView(context, index),
|
|
|
|
Visibility(
|
|
|
|
visible: !_isZoomed,
|
|
|
|
child: AnimatedOpacity(
|
|
|
|
opacity: _isShowDetailPane ? 1 : 0,
|
|
|
|
duration: k.animationDurationNormal,
|
|
|
|
onEnd: () {
|
|
|
|
if (!_isShowDetailPane) {
|
|
|
|
setState(() {
|
|
|
|
_isDetailPaneActive = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
child: Container(
|
|
|
|
alignment: Alignment.topLeft,
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
minHeight: MediaQuery.of(context).size.height),
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
|
|
borderRadius: const BorderRadius.vertical(
|
|
|
|
top: Radius.circular(4)),
|
|
|
|
),
|
|
|
|
margin:
|
|
|
|
EdgeInsets.only(top: _calcDetailPaneOffset(index)),
|
|
|
|
// this visibility widget avoids loading the detail pane
|
|
|
|
// until it's actually opened, otherwise swiping between
|
|
|
|
// photos will slow down severely
|
|
|
|
child: Visibility(
|
|
|
|
visible: _isShowDetailPane,
|
|
|
|
child: ViewerDetailPane(
|
|
|
|
account: widget.account,
|
|
|
|
file: widget.streamFiles[index],
|
|
|
|
album: widget.album,
|
|
|
|
onSlideshowPressed: _onSlideshowPressed,
|
|
|
|
),
|
2021-11-26 11:10:18 +01:00
|
|
|
),
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2022-01-21 19:50:33 +01:00
|
|
|
],
|
|
|
|
),
|
2021-04-10 06:28:12 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildItemView(BuildContext context, int index) {
|
2021-05-06 13:36:20 +02:00
|
|
|
final file = widget.streamFiles[index];
|
|
|
|
if (file_util.isSupportedImageFormat(file)) {
|
|
|
|
return _buildImageView(context, index);
|
|
|
|
} else if (file_util.isSupportedVideoFormat(file)) {
|
|
|
|
return _buildVideoView(context, index);
|
|
|
|
} else {
|
|
|
|
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
|
2021-07-23 22:05:57 +02:00
|
|
|
_pageStates[index]!.itemHeight = 0;
|
2021-05-06 13:36:20 +02:00
|
|
|
return Container();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildImageView(BuildContext context, int index) {
|
2022-04-21 21:13:31 +02:00
|
|
|
return RemoteImageViewer(
|
2021-05-02 16:18:26 +02:00
|
|
|
account: widget.account,
|
|
|
|
file: widget.streamFiles[index],
|
|
|
|
canZoom: _canZoom(),
|
|
|
|
onLoaded: () => _onImageLoaded(index),
|
|
|
|
onHeightChanged: (height) => _updateItemHeight(index, height),
|
|
|
|
onZoomStarted: () {
|
|
|
|
setState(() {
|
|
|
|
_isZoomed = true;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
onZoomEnded: () {
|
|
|
|
setState(() {
|
|
|
|
_isZoomed = false;
|
|
|
|
});
|
|
|
|
},
|
2021-04-10 06:28:12 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-06 13:36:20 +02:00
|
|
|
Widget _buildVideoView(BuildContext context, int index) {
|
|
|
|
return VideoViewer(
|
|
|
|
account: widget.account,
|
|
|
|
file: widget.streamFiles[index],
|
|
|
|
onLoaded: () => _onVideoLoaded(index),
|
|
|
|
onHeightChanged: (height) => _updateItemHeight(index, height),
|
|
|
|
onPlay: _onVideoPlay,
|
|
|
|
onPause: _onVideoPause,
|
|
|
|
isControlVisible: _isShowAppBar && !_isDetailPaneActive,
|
|
|
|
canPlay: !_isDetailPaneActive,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-10 06:28:12 +02:00
|
|
|
bool _onPageContentScrolled(ScrollNotification notification, int index) {
|
|
|
|
if (!_canOpenDetailPane()) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-12-08 21:18:48 +01:00
|
|
|
if (notification is ScrollStartNotification) {
|
|
|
|
_scrollStartPosition =
|
|
|
|
_pageStates[index]?.scrollController.position.pixels;
|
|
|
|
}
|
2021-04-10 06:28:12 +02:00
|
|
|
if (notification is ScrollEndNotification) {
|
2021-12-08 21:18:48 +01:00
|
|
|
_scrollStartPosition = null;
|
2021-07-23 22:05:57 +02:00
|
|
|
final scrollPos = _pageStates[index]!.scrollController.position;
|
2021-04-10 06:28:12 +02:00
|
|
|
if (scrollPos.pixels == 0) {
|
|
|
|
setState(() {
|
|
|
|
_onDetailPaneClosed();
|
|
|
|
});
|
|
|
|
} else if (scrollPos.pixels <
|
2021-04-11 13:06:24 +02:00
|
|
|
_calcDetailPaneOpenedScrollPosition(index) - 1) {
|
2021-04-10 06:28:12 +02:00
|
|
|
if (scrollPos.userScrollDirection == ScrollDirection.reverse) {
|
|
|
|
// upward, open the pane to its minimal size
|
|
|
|
Future.delayed(Duration.zero, () {
|
|
|
|
setState(() {
|
2021-07-31 17:38:26 +02:00
|
|
|
_openDetailPane(_viewerController.currentPage,
|
2021-04-10 06:28:12 +02:00
|
|
|
shouldAnimate: true);
|
|
|
|
});
|
|
|
|
});
|
2021-04-11 13:13:54 +02:00
|
|
|
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
|
2021-04-10 06:28:12 +02:00
|
|
|
// downward, close the pane
|
|
|
|
Future.delayed(Duration.zero, () {
|
2021-07-31 17:38:26 +02:00
|
|
|
_closeDetailPane(_viewerController.currentPage,
|
2021-07-23 22:05:57 +02:00
|
|
|
shouldAnimate: true);
|
2021-04-10 06:28:12 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2021-11-26 11:10:18 +01:00
|
|
|
} else if (notification is ScrollUpdateNotification) {
|
|
|
|
if (!_isShowDetailPane) {
|
|
|
|
Future.delayed(Duration.zero, () {
|
|
|
|
setState(() {
|
|
|
|
_isShowDetailPane = true;
|
2021-12-08 06:59:59 +01:00
|
|
|
_isDetailPaneActive = true;
|
2021-11-26 11:10:18 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
2021-12-08 21:18:48 +01:00
|
|
|
|
|
|
|
if (notification is OverscrollNotification) {
|
|
|
|
if (_scrollStartPosition == 0) {
|
|
|
|
// start at top
|
|
|
|
_overscrollSum += notification.overscroll;
|
2022-02-13 20:34:44 +01:00
|
|
|
if (_overscrollSum < -144) {
|
2021-12-08 21:18:48 +01:00
|
|
|
// and scroll downwards
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_overscrollSum = 0;
|
|
|
|
}
|
2021-04-10 06:28:12 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-05-02 16:18:26 +02:00
|
|
|
void _onImageLoaded(int index) {
|
2021-04-10 06:28:12 +02:00
|
|
|
// currently pageview doesn't pre-load pages, we do it manually
|
|
|
|
// don't pre-load if user already navigated away
|
2021-07-31 17:38:26 +02:00
|
|
|
if (_viewerController.currentPage == index &&
|
2021-07-23 22:05:57 +02:00
|
|
|
!_pageStates[index]!.hasLoaded) {
|
2021-05-02 16:18:26 +02:00
|
|
|
_log.info("[_onImageLoaded] Pre-loading nearby images");
|
2021-04-10 06:28:12 +02:00
|
|
|
if (index > 0) {
|
2021-05-02 16:18:26 +02:00
|
|
|
final prevFile = widget.streamFiles[index - 1];
|
2021-05-06 13:36:20 +02:00
|
|
|
if (file_util.isSupportedImageFormat(prevFile)) {
|
2022-04-21 21:13:31 +02:00
|
|
|
RemoteImageViewer.preloadImage(widget.account, prevFile);
|
2021-05-06 13:36:20 +02:00
|
|
|
}
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
if (index + 1 < widget.streamFiles.length) {
|
2021-05-02 16:18:26 +02:00
|
|
|
final nextFile = widget.streamFiles[index + 1];
|
2021-05-06 13:36:20 +02:00
|
|
|
if (file_util.isSupportedImageFormat(nextFile)) {
|
2022-04-21 21:13:31 +02:00
|
|
|
RemoteImageViewer.preloadImage(widget.account, nextFile);
|
2021-05-06 13:36:20 +02:00
|
|
|
}
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
}
|
2021-08-05 12:38:43 +02:00
|
|
|
setState(() {
|
|
|
|
_pageStates[index]!.hasLoaded = true;
|
|
|
|
_isViewerLoaded = true;
|
|
|
|
});
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
|
2021-05-06 13:36:20 +02:00
|
|
|
void _onVideoLoaded(int index) {
|
2021-08-05 12:38:43 +02:00
|
|
|
setState(() {
|
|
|
|
_pageStates[index]!.hasLoaded = true;
|
|
|
|
_isViewerLoaded = true;
|
|
|
|
});
|
2021-05-06 13:36:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void _onVideoPlay() {
|
|
|
|
setState(() {
|
|
|
|
_setShowActionBar(false);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void _onVideoPause() {
|
|
|
|
setState(() {
|
|
|
|
_setShowActionBar(true);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-04-10 06:28:12 +02:00
|
|
|
/// Called when the page is being built for the first time
|
|
|
|
void _onCreateNewPage(BuildContext context, int index) {
|
|
|
|
_pageStates[index] = _PageState(ScrollController(
|
|
|
|
initialScrollOffset: _isShowDetailPane && !_isClosingDetailPane
|
|
|
|
? _calcDetailPaneOpenedScrollPosition(index)
|
|
|
|
: 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Called when the page is being built after previously moved out of view
|
|
|
|
void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
|
|
|
|
if (_isShowDetailPane && !_isClosingDetailPane) {
|
2022-06-20 13:49:58 +02:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
2021-07-23 22:05:57 +02:00
|
|
|
if (_pageStates[index]!.itemHeight != null) {
|
2021-04-10 06:28:12 +02:00
|
|
|
setState(() {
|
|
|
|
_openDetailPane(index);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2022-06-20 13:49:58 +02:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
2021-07-23 22:05:57 +02:00
|
|
|
_pageStates[index]!.scrollController.jumpTo(0);
|
2021-04-10 06:28:12 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-25 11:17:19 +01:00
|
|
|
Future<void> _onFavoritePressed(int index) async {
|
|
|
|
if (_pageStates[index]!.isProcessingFavorite) {
|
|
|
|
_log.fine("[_onFavoritePressed] Process ongoing, ignored");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
|
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
|
|
setState(() {
|
|
|
|
_pageStates[index]!.favoriteOverride = true;
|
|
|
|
});
|
|
|
|
_pageStates[index]!.isProcessingFavorite = true;
|
|
|
|
try {
|
|
|
|
await NotifiedAction(
|
|
|
|
() => UpdateProperty(c.fileRepo)(
|
|
|
|
widget.account,
|
|
|
|
file,
|
|
|
|
favorite: true,
|
|
|
|
),
|
|
|
|
null,
|
|
|
|
L10n.global().favoriteSuccessNotification,
|
|
|
|
failureText: L10n.global().favoriteFailureNotification,
|
|
|
|
)();
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout(
|
|
|
|
"[_onFavoritePressed] Failed while UpdateProperty", e, stackTrace);
|
|
|
|
setState(() {
|
|
|
|
_pageStates[index]!.favoriteOverride = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
_pageStates[index]!.isProcessingFavorite = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _onUnfavoritePressed(int index) async {
|
|
|
|
if (_pageStates[index]!.isProcessingFavorite) {
|
|
|
|
_log.fine("[_onUnfavoritePressed] Process ongoing, ignored");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
|
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
|
|
setState(() {
|
|
|
|
_pageStates[index]!.favoriteOverride = false;
|
|
|
|
});
|
|
|
|
_pageStates[index]!.isProcessingFavorite = true;
|
|
|
|
try {
|
|
|
|
await NotifiedAction(
|
|
|
|
() => UpdateProperty(c.fileRepo)(
|
|
|
|
widget.account,
|
|
|
|
file,
|
|
|
|
favorite: false,
|
|
|
|
),
|
|
|
|
null,
|
|
|
|
L10n.global().unfavoriteSuccessNotification,
|
|
|
|
failureText: L10n.global().unfavoriteFailureNotification,
|
|
|
|
)();
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout(
|
|
|
|
"[_onUnfavoritePressed] Failed while UpdateProperty", e, stackTrace);
|
|
|
|
setState(() {
|
|
|
|
_pageStates[index]!.favoriteOverride = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
_pageStates[index]!.isProcessingFavorite = false;
|
|
|
|
}
|
|
|
|
|
2021-04-10 06:28:12 +02:00
|
|
|
void _onDetailsPressed() {
|
|
|
|
if (!_isDetailPaneActive) {
|
|
|
|
setState(() {
|
2021-07-31 17:38:26 +02:00
|
|
|
_openDetailPane(_viewerController.currentPage, shouldAnimate: true);
|
2021-04-10 06:28:12 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-10 17:06:04 +02:00
|
|
|
void _onSharePressed(BuildContext context) {
|
2021-07-31 17:38:26 +02:00
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
2021-10-08 20:39:53 +02:00
|
|
|
ShareHandler(
|
|
|
|
context: context,
|
|
|
|
).shareFiles(widget.account, [file]);
|
2021-07-10 17:06:04 +02:00
|
|
|
}
|
|
|
|
|
2022-07-12 22:11:27 +02:00
|
|
|
void _onEditPressed(BuildContext context) {
|
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
|
|
|
if (!file_util.isSupportedImageFormat(file)) {
|
|
|
|
_log.shout("[_onEditPressed] Video file not supported");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_log.info("[_onEditPressed] Edit file: ${file.path}");
|
|
|
|
Navigator.of(context).pushNamed(ImageEditor.routeName,
|
|
|
|
arguments: ImageEditorArguments(widget.account, file));
|
|
|
|
}
|
|
|
|
|
2022-05-04 10:42:46 +02:00
|
|
|
void _onEnhancePressed(BuildContext context) {
|
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
|
|
|
if (!file_util.isSupportedImageFormat(file)) {
|
|
|
|
_log.shout("[_onEnhancePressed] Video file not supported");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_log.info("[_onEnhancePressed] Enhance file: ${file.path}");
|
|
|
|
EnhanceHandler(
|
|
|
|
account: widget.account,
|
|
|
|
file: file,
|
|
|
|
)(context);
|
|
|
|
}
|
|
|
|
|
2021-09-28 22:56:44 +02:00
|
|
|
void _onDownloadPressed() {
|
2021-07-31 17:38:26 +02:00
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
2021-04-10 06:28:12 +02:00
|
|
|
_log.info("[_onDownloadPressed] Downloading file: ${file.path}");
|
2021-09-28 22:56:44 +02:00
|
|
|
DownloadHandler().downloadFiles(widget.account, [file]);
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void _onDeletePressed(BuildContext context) async {
|
2021-07-31 17:38:26 +02:00
|
|
|
final file = widget.streamFiles[_viewerController.currentPage];
|
2021-04-10 06:28:12 +02:00
|
|
|
_log.info("[_onDeletePressed] Removing file: ${file.path}");
|
2021-12-02 11:47:37 +01:00
|
|
|
final count = await RemoveSelectionHandler()(
|
|
|
|
account: widget.account,
|
|
|
|
selectedFiles: [file],
|
|
|
|
isRemoveOpened: true,
|
2022-04-22 21:09:44 +02:00
|
|
|
isMoveToTrash: true,
|
2021-12-02 11:47:37 +01:00
|
|
|
);
|
2022-07-08 16:52:18 +02:00
|
|
|
if (count > 0 && mounted) {
|
2021-04-10 06:28:12 +02:00
|
|
|
Navigator.of(context).pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-14 23:00:24 +02:00
|
|
|
void _onSlideshowPressed() async {
|
|
|
|
final result = await showDialog<SlideshowConfig>(
|
|
|
|
context: context,
|
|
|
|
builder: (_) => SlideshowDialog(
|
2021-10-27 22:40:54 +02:00
|
|
|
duration: Duration(seconds: Pref().getSlideshowDurationOr(5)),
|
|
|
|
isShuffle: Pref().isSlideshowShuffleOr(false),
|
|
|
|
isRepeat: Pref().isSlideshowRepeatOr(false),
|
2021-09-14 23:00:24 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
if (result == null) {
|
2021-08-24 14:56:14 +02:00
|
|
|
return;
|
|
|
|
}
|
2022-07-28 18:59:26 +02:00
|
|
|
unawaited(Pref().setSlideshowDuration(result.duration.inSeconds));
|
|
|
|
unawaited(Pref().setSlideshowShuffle(result.isShuffle));
|
|
|
|
unawaited(Pref().setSlideshowRepeat(result.isRepeat));
|
|
|
|
unawaited(
|
|
|
|
Navigator.of(context).pushNamed(
|
|
|
|
SlideshowViewer.routeName,
|
|
|
|
arguments: SlideshowViewerArguments(widget.account, widget.streamFiles,
|
|
|
|
_viewerController.currentPage, result),
|
|
|
|
),
|
2021-09-14 23:00:24 +02:00
|
|
|
);
|
2021-08-24 14:56:14 +02:00
|
|
|
}
|
|
|
|
|
2021-04-10 06:28:12 +02:00
|
|
|
double _calcDetailPaneOffset(int index) {
|
|
|
|
if (_pageStates[index]?.itemHeight == null) {
|
|
|
|
return MediaQuery.of(context).size.height;
|
|
|
|
} else {
|
2021-07-23 22:05:57 +02:00
|
|
|
return _pageStates[index]!.itemHeight! +
|
|
|
|
(MediaQuery.of(context).size.height -
|
|
|
|
_pageStates[index]!.itemHeight!) /
|
2021-04-10 06:28:12 +02:00
|
|
|
2 -
|
|
|
|
4;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
double _calcDetailPaneOpenedScrollPosition(int index) {
|
|
|
|
// distance of the detail pane from the top edge
|
|
|
|
const distanceFromTop = 196;
|
|
|
|
return max(_calcDetailPaneOffset(index) - distanceFromTop, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _updateItemHeight(int index, double height) {
|
2021-07-23 22:05:57 +02:00
|
|
|
if (_pageStates[index]!.itemHeight != height) {
|
2021-04-10 06:28:12 +02:00
|
|
|
_log.fine("[_updateItemHeight] New height of item#$index: $height");
|
|
|
|
setState(() {
|
2021-07-23 22:05:57 +02:00
|
|
|
_pageStates[index]!.itemHeight = height;
|
2021-04-10 06:28:12 +02:00
|
|
|
if (_isDetailPaneActive) {
|
|
|
|
_openDetailPane(index);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _setShowActionBar(bool flag) {
|
|
|
|
_isShowAppBar = flag;
|
2021-05-06 16:53:15 +02:00
|
|
|
if (flag) {
|
2021-09-15 17:13:38 +02:00
|
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
|
|
overlays: SystemUiOverlay.values);
|
2021-05-06 16:53:15 +02:00
|
|
|
} else {
|
2021-09-15 17:13:38 +02:00
|
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
2021-05-06 16:53:15 +02:00
|
|
|
}
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void _openDetailPane(int index, {bool shouldAnimate = false}) {
|
|
|
|
if (!_canOpenDetailPane()) {
|
|
|
|
_log.warning("[_openDetailPane] Can't open detail pane right now");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_isShowDetailPane = true;
|
|
|
|
_isDetailPaneActive = true;
|
|
|
|
if (shouldAnimate) {
|
2021-07-23 22:05:57 +02:00
|
|
|
_pageStates[index]!.scrollController.animateTo(
|
2021-04-10 06:28:12 +02:00
|
|
|
_calcDetailPaneOpenedScrollPosition(index),
|
|
|
|
duration: k.animationDurationNormal,
|
|
|
|
curve: Curves.easeOut);
|
|
|
|
} else {
|
2021-07-23 22:05:57 +02:00
|
|
|
_pageStates[index]!
|
2021-04-10 06:28:12 +02:00
|
|
|
.scrollController
|
|
|
|
.jumpTo(_calcDetailPaneOpenedScrollPosition(index));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _closeDetailPane(int index, {bool shouldAnimate = false}) {
|
|
|
|
_isClosingDetailPane = true;
|
|
|
|
if (shouldAnimate) {
|
2021-07-23 22:05:57 +02:00
|
|
|
_pageStates[index]!.scrollController.animateTo(0,
|
2021-04-10 06:28:12 +02:00
|
|
|
duration: k.animationDurationNormal, curve: Curves.easeOut);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _onDetailPaneClosed() {
|
|
|
|
_isShowDetailPane = false;
|
|
|
|
_isClosingDetailPane = false;
|
|
|
|
}
|
|
|
|
|
2021-05-02 16:18:26 +02:00
|
|
|
bool _canSwitchPage() => !_isZoomed;
|
|
|
|
bool _canOpenDetailPane() => !_isZoomed;
|
2021-04-10 06:28:12 +02:00
|
|
|
bool _canZoom() => !_isDetailPaneActive;
|
|
|
|
|
|
|
|
var _isShowAppBar = true;
|
|
|
|
|
|
|
|
var _isShowDetailPane = false;
|
|
|
|
var _isDetailPaneActive = false;
|
|
|
|
var _isClosingDetailPane = false;
|
|
|
|
|
2021-05-02 16:18:26 +02:00
|
|
|
var _isZoomed = false;
|
2021-04-10 06:28:12 +02:00
|
|
|
|
2021-07-31 17:38:26 +02:00
|
|
|
final _viewerController = HorizontalPageViewerController();
|
|
|
|
bool _isViewerLoaded = false;
|
2021-04-10 06:28:12 +02:00
|
|
|
final _pageStates = <int, _PageState>{};
|
|
|
|
|
2021-12-08 21:18:48 +01:00
|
|
|
double? _scrollStartPosition;
|
|
|
|
var _overscrollSum = 0.0;
|
|
|
|
|
2021-04-10 06:28:12 +02:00
|
|
|
static final _log = Logger("widget.viewer._ViewerState");
|
2021-07-31 17:38:26 +02:00
|
|
|
|
|
|
|
static const _viewportFraction = 1.05;
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class _PageState {
|
|
|
|
_PageState(this.scrollController);
|
|
|
|
|
|
|
|
ScrollController scrollController;
|
2021-07-23 22:05:57 +02:00
|
|
|
double? itemHeight;
|
2021-05-02 16:18:26 +02:00
|
|
|
bool hasLoaded = false;
|
2022-01-25 11:17:19 +01:00
|
|
|
|
|
|
|
bool isProcessingFavorite = false;
|
|
|
|
bool? favoriteOverride;
|
2021-04-10 06:28:12 +02:00
|
|
|
}
|