aves_mio1/lib/widgets/viewer/visual/entry_page_view.dart.ok
FabioMich66 084fa184da
Some checks failed
Quality check / Flutter analysis (push) Has been cancelled
Quality check / CodeQL analysis (java-kotlin) (push) Has been cancelled
ok con video e foto in galleria aves
2026-03-17 12:19:38 +01:00

562 lines
20 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/widgets/viewer/visual/entry_page_view.dart
import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/widget_outline.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/viewer/view_state.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/media_session_service.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/home_widget.dart';
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/view/conductor.dart';
import 'package:aves/widgets/viewer/visual/error.dart';
import 'package:aves/widgets/viewer/visual/raster.dart';
import 'package:aves/widgets/viewer/visual/vector.dart';
import 'package:aves/widgets/viewer/visual/video/cover.dart';
import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart';
import 'package:aves/widgets/viewer/visual/video/swipe_action.dart';
import 'package:aves/widgets/viewer/visual/video/video_view.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_model/aves_model.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Factory remota per controller video unificato (remoti + locali tramite Conductor)
import 'package:aves/widgets/viewer/video/remote_video_factory.dart';
class EntryPageView extends StatefulWidget {
final AvesEntry mainEntry, pageEntry;
final ViewerController viewerController;
final VoidCallback? onDisposed;
static const decorationCheckSize = 20.0;
static const rasterMaxScale = ScaleLevel(factor: 5);
static const vectorMaxScale = ScaleLevel(factor: 25);
const EntryPageView({
super.key,
required this.mainEntry,
required this.pageEntry,
required this.viewerController,
this.onDisposed,
});
@override
State<EntryPageView> createState() => _EntryPageViewState();
}
class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateMixin {
late ValueNotifier<ViewState> _viewStateNotifier;
late AvesMagnifierController _magnifierController;
final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
OverlayEntry? _actionFeedbackOverlayEntry;
AvesEntry get mainEntry => widget.mainEntry;
AvesEntry get entry => widget.pageEntry;
ViewerController get viewerController => widget.viewerController;
bool get _isRemote => entry.origin == 1;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant EntryPageView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.pageEntry != widget.pageEntry) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_unregisterWidget(widget);
widget.onDisposed?.call();
_actionFeedbackChildNotifier.dispose();
super.dispose();
}
void _registerWidget(EntryPageView widget) {
final entry = widget.pageEntry;
_viewStateNotifier = context.read<ViewStateConductor>().getOrCreateController(entry).viewStateNotifier;
_magnifierController = AvesMagnifierController();
_subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
if (entry.isVideo) {
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
}
// idempotente
context.read<VideoConductor>().registerRemoteFactory(RemoteVideoControllerFactory());
viewerController.startAutopilotAnimation(
vsync: this,
onUpdate: ({required scaleLevel}) {
final boundaries = _magnifierController.scaleBoundaries;
if (boundaries != null) {
final scale = boundaries.scaleForLevel(scaleLevel);
_magnifierController.update(scale: scale, source: ChangeSource.animation);
}
},
);
}
void _unregisterWidget(EntryPageView oldWidget) {
viewerController.stopAutopilotAnimation(vsync: this);
_magnifierController.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
}
@override
Widget build(BuildContext context) {
Widget child = AnimatedBuilder(
animation: entry.visualChangeNotifier,
builder: (context, child) {
Widget? child;
// Pipeline unificata:
// - SVG → _buildSvgView()
// - Video → _buildVideoView()
// - Raster (locali + remoti) → _buildRasterView()
if (entry.isSvg) {
child = _buildSvgView();
} else if (!entry.displaySize.isEmpty) {
if (entry.isVideo) {
child = _buildVideoView();
} else if (entry.isDecodingSupported || _isRemote) {
child = _buildRasterView();
}
}
child ??= ErrorView(
entry: entry,
onTap: _onTap,
);
return child;
},
);
if (!settings.viewerUseCutout) {
child = SafeCutoutArea(
child: ClipRect(child: child),
);
}
final animate = context.select<Settings, bool>((v) => v.animate);
if (animate) {
child = Consumer<EntryHeroInfo?>(
builder: (context, info, child) => Hero(
tag: info != null && info.entry == mainEntry ? info.tag : hashCode,
transitionOnUserGestures: true,
child: child!,
),
child: child,
);
}
return child;
}
// RASTER (locali + remoti)
Widget _buildRasterView() {
return _buildMagnifier(
applyScale: false,
child: RasterImageView(
entry: entry,
viewStateNotifier: _viewStateNotifier,
errorBuilder: (context, error, stackTrace) => ErrorView(
entry: entry,
onTap: _onTap,
),
),
);
}
Widget _buildSvgView() {
return _buildMagnifier(
maxScale: EntryPageView.vectorMaxScale,
scaleStateCycle: _vectorScaleStateCycle,
applyScale: false,
child: VectorImageView(
entry: entry,
viewStateNotifier: _viewStateNotifier,
errorBuilder: (context, error, stackTrace) => ErrorView(
entry: entry,
onTap: _onTap,
),
),
);
}
Widget _buildVideoView() {
final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return const SizedBox();
return ValueListenableBuilder<double?>(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar);
final isPureVideo = entry.isPureVideo;
return Selector<Settings, (bool, bool, bool)>(
selector: (context, s) => (
isPureVideo && s.videoGestureDoubleTapTogglePlay,
isPureVideo && s.videoGestureSideDoubleTapSeek,
isPureVideo && s.videoGestureVerticalDragBrightnessVolume,
),
builder: (context, s, child) {
final (playGesture, seekGesture, useVerticalDragGesture) = s;
final useTapGesture = playGesture || seekGesture;
MagnifierDoubleTapCallback? onDoubleTap;
MagnifierGestureScaleStartCallback? onScaleStart;
MagnifierGestureScaleUpdateCallback? onScaleUpdate;
MagnifierGestureScaleEndCallback? onScaleEnd;
if (useTapGesture) {
void _applyAction(EntryAction action, {IconData? Function()? icon}) {
_actionFeedbackChildNotifier.value = DecoratedIcon(
icon?.call() ?? action.getIconData(),
size: 48,
color: Colors.white,
shadows: const [Shadow(color: Colors.black, blurRadius: 4)],
);
VideoActionNotification(
controller: videoController,
entry: entry,
action: action,
).dispatch(context);
}
onDoubleTap = (alignment) {
final x = alignment.x;
if (seekGesture) {
final sideRatio = _getSideRatio();
if (sideRatio != null) {
if (x < sideRatio) {
_applyAction(EntryAction.videoReplay10);
return true;
} else if (x > 1 - sideRatio) {
_applyAction(EntryAction.videoSkip10);
return true;
}
}
}
if (playGesture) {
_applyAction(
EntryAction.videoTogglePlay,
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
);
return true;
}
return false;
};
}
if (useVerticalDragGesture) {
SwipeAction? swipeAction;
var move = Offset.zero;
var dropped = false;
double? startValue;
ValueNotifier<double?>? valueNotifier;
onScaleStart = (details, doubleTap, boundaries) {
dropped = details.pointerCount > 1 || doubleTap;
if (dropped) return;
startValue = null;
valueNotifier = ValueNotifier<double?>(null);
final alignmentX = details.focalPoint.dx / boundaries.viewportSize.width;
final action = alignmentX > .5 ? SwipeAction.volume : SwipeAction.brightness;
action.get().then((v) => startValue = v);
swipeAction = action;
move = Offset.zero;
_actionFeedbackOverlayEntry = OverlayEntry(
builder: (context) => SwipeActionFeedback(
action: action,
valueNotifier: valueNotifier!,
),
);
Overlay.of(context).insert(_actionFeedbackOverlayEntry!);
};
onScaleUpdate = (details) {
if (valueNotifier == null) return false;
move += details.focalPointDelta;
dropped |= details.pointerCount > 1;
if (valueNotifier!.value == null) {
dropped |= MagnifierGestureRecognizer.isXPan(move);
}
if (dropped) return false;
final _startValue = startValue;
if (_startValue != null) {
final double value = (_startValue - move.dy / SwipeActionFeedback.height).clamp(0, 1);
valueNotifier!.value = value;
swipeAction?.set(value);
}
return true;
};
onScaleEnd = (details) {
valueNotifier?.dispose();
_actionFeedbackOverlayEntry
?..remove()
..dispose();
_actionFeedbackOverlayEntry = null;
};
}
Widget videoChild = Stack(
children: [
_buildMagnifier(
displaySize: videoDisplaySize,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onDoubleTap: onDoubleTap,
child: VideoView(
entry: entry,
controller: videoController,
),
),
VideoSubtitles(
entry: entry,
controller: videoController,
viewStateNotifier: _viewStateNotifier,
),
if (useTapGesture)
ValueListenableBuilder<Widget?>(
valueListenable: _actionFeedbackChildNotifier,
builder: (context, feedbackChild, child) => ActionFeedback(
child: feedbackChild,
),
),
],
);
if (useVerticalDragGesture) {
final scope = MagnifierGestureDetectorScope.maybeOf(context);
if (scope != null) {
videoChild = scope.copyWith(
acceptPointerEvent: MagnifierGestureRecognizer.isYPan,
child: videoChild,
);
}
}
return Stack(
fit: StackFit.expand,
children: [
videoChild,
VideoCover(
mainEntry: mainEntry,
pageEntry: entry,
magnifierController: _magnifierController,
videoController: videoController,
videoDisplaySize: videoDisplaySize,
onTap: _onTap,
magnifierBuilder: (coverController, coverSize, videoCoverUriImage) => _buildMagnifier(
controller: coverController,
displaySize: coverSize,
onDoubleTap: onDoubleTap,
child: Image(image: videoCoverUriImage),
),
),
],
);
},
);
},
);
}
/// Wrapper del Magnifier con bound **remotefriendly**
Widget _buildMagnifier({
AvesMagnifierController? controller,
Size? displaySize,
ScaleLevel maxScale = EntryPageView.rasterMaxScale,
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
bool applyScale = true,
MagnifierGestureScaleStartCallback? onScaleStart,
MagnifierGestureScaleUpdateCallback? onScaleUpdate,
MagnifierGestureScaleEndCallback? onScaleEnd,
MagnifierDoubleTapCallback? onDoubleTap,
required Widget child,
}) {
final isWallpaperMode = context.read<ValueNotifier<AppMode>>().value == AppMode.setWallpaper;
const contained = ScaleLevel(ref: ScaleReference.contained);
const covered = ScaleLevel(ref: ScaleReference.covered);
// REMOTO:
// - minScale sotto al fit per abilitare pinchout
// - initialScale al fit (poi RasterImageView lo “cappa” a <= 1.0)
final minScale = isWallpaperMode
? covered
: (_isRemote ? const ScaleLevel(ref: ScaleReference.contained, factor: .5) : contained);
final initialScale = _isRemote ? contained : viewerController.initialScale;
return ValueListenableBuilder<bool>(
valueListenable: AvesApp.canGestureToOtherApps,
builder: (context, canGestureToOtherApps, childWidget) {
return AvesMagnifier(
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
controller: controller ?? _magnifierController,
contentSize: displaySize ?? entry.displaySize,
allowOriginalScaleBeyondRange: !isWallpaperMode,
allowDoubleTap: _allowDoubleTap,
minScale: minScale,
maxScale: maxScale,
initialScale: initialScale,
scaleStateCycle: scaleStateCycle,
applyScale: applyScale,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onFling: _onFling,
onTap: (context, _, alignment, _) {
if (context.mounted) {
_onTap(alignment: alignment);
}
},
onLongPress: canGestureToOtherApps ? _startGlobalDrag : null,
onDoubleTap: onDoubleTap,
child: childWidget!,
);
},
child: child,
);
}
Future<void> _startGlobalDrag() async {
const dragShadowSize = Size.square(128);
final cornerRadiusPx = await deviceService.getWidgetCornerRadiusPx();
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final brightness = Theme.of(context).brightness;
final outline = await WidgetOutline.systemBlackAndWhite.color(brightness);
final dragShadowBytes = await HomeWidgetPainter(
entry: entry,
devicePixelRatio: devicePixelRatio,
).drawWidget(
sizeDip: dragShadowSize,
cornerRadiusPx: cornerRadiusPx,
outline: outline,
shape: WidgetShape.rrect,
);
await windowService.startGlobalDrag(entry.uri, entry.bestTitle, dragShadowSize, dragShadowBytes);
}
void _onFling(AxisDirection direction) {
const animate = true;
switch (direction) {
case AxisDirection.left:
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
case AxisDirection.right:
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
case AxisDirection.up:
PopVisualNotification().dispatch(context);
case AxisDirection.down:
ShowInfoPageNotification().dispatch(context);
}
}
Notification? _handleSideSingleTap(Alignment? alignment) {
if (settings.viewerGestureSideTapNext && alignment != null) {
final x = alignment.x;
final sideRatio = _getSideRatio();
if (sideRatio != null) {
const animate = false;
if (x < sideRatio) {
return context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate);
} else if (x > 1 - sideRatio) {
return context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate);
}
}
}
return null;
}
void _onTap({Alignment? alignment}) => (_handleSideSingleTap(alignment) ?? const ToggleOverlayNotification()).dispatch(context);
bool _allowDoubleTap(Alignment alignment) {
if (entry.isVideo && settings.videoGestureSideDoubleTapSeek) {
return true;
}
final actionNotification = _handleSideSingleTap(alignment);
return actionNotification == null;
}
void _onMediaCommand(MediaCommandEvent event) {
final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return;
switch (event.command) {
case MediaCommand.play:
videoController.play();
case MediaCommand.pause:
videoController.pause();
case MediaCommand.skipToNext:
ShowNextVideoNotification().dispatch(context);
case MediaCommand.skipToPrevious:
ShowPreviousVideoNotification().dispatch(context);
case MediaCommand.stop:
videoController.pause();
case MediaCommand.seek:
if (event is MediaSeekCommandEvent) {
videoController.seekTo(event.position);
}
}
}
void _onViewStateChanged(MagnifierState v) {
if (!mounted) return;
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(
position: v.position,
scale: v.scale,
);
}
void _onViewScaleBoundariesChanged(ScaleBoundaries v) {
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(
viewportSize: v.viewportSize,
contentSize: v.contentSize,
);
}
double? _getSideRatio() {
if (!mounted) return null;
final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait;
return isPortrait ? 1 / 6 : 1 / 8;
}
static ScaleState _vectorScaleStateCycle(ScaleState actual) {
switch (actual) {
case ScaleState.initial:
return ScaleState.covering;
default:
return ScaleState.initial;
}
}
}