viewer: video cover magnifier state transition
This commit is contained in:
parent
a1b8d313e8
commit
2628192e06
2 changed files with 101 additions and 73 deletions
|
@ -18,13 +18,14 @@ class MagnifierController {
|
||||||
late ScaleStateChange _currentScaleState, previousScaleState;
|
late ScaleStateChange _currentScaleState, previousScaleState;
|
||||||
|
|
||||||
MagnifierController({
|
MagnifierController({
|
||||||
Offset initialPosition = Offset.zero,
|
MagnifierState? initialState,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
initial = MagnifierState(
|
initial = initialState ??
|
||||||
position: initialPosition,
|
const MagnifierState(
|
||||||
scale: null,
|
position: Offset.zero,
|
||||||
source: ChangeSource.internal,
|
scale: null,
|
||||||
);
|
source: ChangeSource.internal,
|
||||||
|
);
|
||||||
previousState = initial;
|
previousState = initial;
|
||||||
_currentState = initial;
|
_currentState = initial;
|
||||||
_setState(initial);
|
_setState(initial);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:aves/widgets/viewer/visual/state.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart';
|
import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/vector.dart';
|
import 'package:aves/widgets/viewer/visual/vector.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/video.dart';
|
import 'package:aves/widgets/viewer/visual/video.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -51,6 +52,13 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
late ImageStreamListener _videoCoverStreamListener;
|
late ImageStreamListener _videoCoverStreamListener;
|
||||||
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
|
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
|
||||||
|
|
||||||
|
MagnifierController? _dismissedCoverMagnifierController;
|
||||||
|
|
||||||
|
MagnifierController get dismissedCoverMagnifierController {
|
||||||
|
_dismissedCoverMagnifierController ??= MagnifierController();
|
||||||
|
return _dismissedCoverMagnifierController!;
|
||||||
|
}
|
||||||
|
|
||||||
AvesEntry get mainEntry => widget.mainEntry;
|
AvesEntry get mainEntry => widget.mainEntry;
|
||||||
|
|
||||||
AvesEntry get entry => widget.pageEntry;
|
AvesEntry get entry => widget.pageEntry;
|
||||||
|
@ -176,91 +184,110 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
Widget _buildVideoView() {
|
Widget _buildVideoView() {
|
||||||
final videoController = context.read<VideoConductor>().getController(entry);
|
final videoController = context.read<VideoConductor>().getController(entry);
|
||||||
if (videoController == null) return const SizedBox();
|
if (videoController == null) return const SizedBox();
|
||||||
return Stack(
|
return ValueListenableBuilder<double>(
|
||||||
fit: StackFit.expand,
|
valueListenable: videoController.sarNotifier,
|
||||||
children: [
|
builder: (context, sar, child) {
|
||||||
ValueListenableBuilder<double>(
|
final videoDisplaySize = entry.videoDisplaySize(sar);
|
||||||
valueListenable: videoController.sarNotifier,
|
return Stack(
|
||||||
builder: (context, sar, child) {
|
fit: StackFit.expand,
|
||||||
return Stack(
|
children: [
|
||||||
children: [
|
Stack(
|
||||||
_buildMagnifier(
|
children: [
|
||||||
displaySize: entry.videoDisplaySize(sar),
|
_buildMagnifier(
|
||||||
child: VideoView(
|
displaySize: videoDisplaySize,
|
||||||
entry: entry,
|
child: VideoView(
|
||||||
controller: videoController,
|
entry: entry,
|
||||||
),
|
controller: videoController,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
VideoSubtitles(
|
||||||
|
controller: videoController,
|
||||||
|
viewStateNotifier: _viewStateNotifier,
|
||||||
|
),
|
||||||
|
if (settings.videoShowRawTimedText)
|
||||||
VideoSubtitles(
|
VideoSubtitles(
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
viewStateNotifier: _viewStateNotifier,
|
viewStateNotifier: _viewStateNotifier,
|
||||||
|
debugMode: true,
|
||||||
),
|
),
|
||||||
if (settings.videoShowRawTimedText)
|
],
|
||||||
VideoSubtitles(
|
),
|
||||||
controller: videoController,
|
_buildVideoCover(videoController, videoDisplaySize),
|
||||||
viewStateNotifier: _viewStateNotifier,
|
],
|
||||||
debugMode: true,
|
);
|
||||||
),
|
},
|
||||||
],
|
);
|
||||||
);
|
}
|
||||||
}),
|
|
||||||
// fade out image to ease transition with the player
|
|
||||||
StreamBuilder<VideoStatus>(
|
|
||||||
stream: videoController.statusStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final showCover = !videoController.isReady;
|
|
||||||
return IgnorePointer(
|
|
||||||
ignoring: !showCover,
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: showCover ? 1 : 0,
|
|
||||||
curve: Curves.easeInCirc,
|
|
||||||
duration: Durations.viewerVideoPlayerTransition,
|
|
||||||
child: ValueListenableBuilder<ImageInfo?>(
|
|
||||||
valueListenable: _videoCoverInfoNotifier,
|
|
||||||
builder: (context, videoCoverInfo, child) {
|
|
||||||
if (videoCoverInfo == null) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: _onTap,
|
|
||||||
child: ThumbnailImage(
|
|
||||||
entry: entry,
|
|
||||||
extent: context.select<MediaQueryData, double>((mq) => mq.size.shortestSide),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
showLoadingBackground: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// full cover image
|
StreamBuilder<VideoStatus> _buildVideoCover(AvesVideoController videoController, Size videoDisplaySize) {
|
||||||
return _buildMagnifier(
|
// fade out image to ease transition with the player
|
||||||
displaySize: Size(
|
return StreamBuilder<VideoStatus>(
|
||||||
videoCoverInfo.image.width.toDouble(),
|
stream: videoController.statusStream,
|
||||||
videoCoverInfo.image.height.toDouble(),
|
builder: (context, snapshot) {
|
||||||
),
|
final showCover = !videoController.isReady;
|
||||||
child: Image(
|
return IgnorePointer(
|
||||||
image: entry.uriImage,
|
ignoring: !showCover,
|
||||||
),
|
child: AnimatedOpacity(
|
||||||
);
|
opacity: showCover ? 1 : 0,
|
||||||
},
|
curve: Curves.easeInCirc,
|
||||||
),
|
duration: Durations.viewerVideoPlayerTransition,
|
||||||
),
|
child: ValueListenableBuilder<ImageInfo?>(
|
||||||
);
|
valueListenable: _videoCoverInfoNotifier,
|
||||||
},
|
builder: (context, videoCoverInfo, child) {
|
||||||
),
|
if (videoCoverInfo != null) {
|
||||||
],
|
// full cover image may have a different size and different aspect ratio
|
||||||
|
final coverSize = Size(
|
||||||
|
videoCoverInfo.image.width.toDouble(),
|
||||||
|
videoCoverInfo.image.height.toDouble(),
|
||||||
|
);
|
||||||
|
// when the cover is the same size as the video itself
|
||||||
|
// (which is often the case when the cover is not embedded but just a frame),
|
||||||
|
// we can reuse the same magnifier and preserve its state when switching from cover to video
|
||||||
|
final coverController = showCover || coverSize == videoDisplaySize ? _magnifierController : dismissedCoverMagnifierController;
|
||||||
|
return _buildMagnifier(
|
||||||
|
controller: coverController,
|
||||||
|
displaySize: coverSize,
|
||||||
|
child: Image(
|
||||||
|
image: entry.uriImage,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// default to cached thumbnail, if any
|
||||||
|
final extent = entry.cachedThumbnails.firstOrNull?.key.extent;
|
||||||
|
if (extent != null && extent > 0) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _onTap,
|
||||||
|
child: ThumbnailImage(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
showLoadingBackground: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMagnifier({
|
Widget _buildMagnifier({
|
||||||
|
MagnifierController? controller,
|
||||||
|
Size? displaySize,
|
||||||
ScaleLevel maxScale = maxScale,
|
ScaleLevel maxScale = maxScale,
|
||||||
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
|
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
|
||||||
bool applyScale = true,
|
bool applyScale = true,
|
||||||
Size? displaySize,
|
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
return Magnifier(
|
return Magnifier(
|
||||||
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
|
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
|
||||||
key: ValueKey('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
key: ValueKey('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
||||||
controller: _magnifierController,
|
controller: controller ?? _magnifierController,
|
||||||
childSize: displaySize ?? entry.displaySize,
|
childSize: displaySize ?? entry.displaySize,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
maxScale: maxScale,
|
maxScale: maxScale,
|
||||||
|
|
Loading…
Reference in a new issue