#469 improved cutout area handling

This commit is contained in:
Thibault Deckers 2023-01-04 16:18:28 +01:00
parent c693055721
commit 31c14febdc
23 changed files with 415 additions and 298 deletions

View file

@ -13,6 +13,10 @@ All notable changes to this project will be documented in this file.
- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment` - editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment`
### Fixed
- transition between collection and viewer when cutout area is not used
## <a id="v1.7.8"></a>[v1.7.8] - 2022-12-20 ## <a id="v1.7.8"></a>[v1.7.8] - 2022-12-20
### Added ### Added

View file

@ -191,7 +191,7 @@ dependencies {
implementation 'com.drewnoakes:metadata-extractor:2.18.0' implementation 'com.drewnoakes:metadata-extractor:2.18.0'
implementation 'com.github.bumptech.glide:glide:4.14.2' implementation 'com.github.bumptech.glide:glide:4.14.2'
// SLF4J implementation for `mp4parser` // SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.3' implementation 'org.slf4j:slf4j-simple:2.0.6'
// forked, built by JitPack: // forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory // - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls.window
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import android.view.WindowManager import android.view.WindowManager
import deckers.thibault.aves.utils.getDisplayCompat
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -42,25 +43,34 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(true) result.success(true)
} }
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
} }
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
if (use == null) { result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
result.error("setCutoutMode-args", "missing arguments", null)
return return
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val mode = if (use) { activity.getDisplayCompat()?.cutout
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } else {
} else { activity.window.decorView.rootWindowInsets.displayCutout
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
} }
result.success(true) if (cutout == null) {
result.error("getCutoutInsets-null", "cutout insets are null", null)
return
}
val density = activity.resources.displayMetrics.density
result.success(
hashMapOf(
"left" to cutout.safeInsetLeft / density,
"top" to cutout.safeInsetTop / density,
"right" to cutout.safeInsetRight / density,
"bottom" to cutout.safeInsetBottom / density,
)
)
} }
} }

View file

@ -17,11 +17,11 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
result.success(false) result.success(false)
} }
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(false) result.success(false)
} }
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
result.success(false) result.success(HashMap<String, Any>())
} }
} }

View file

@ -15,8 +15,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn) "keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked) "isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode) "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode) "getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -37,9 +37,9 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result) abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result)
abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) abstract fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>() private val LOG_TAG = LogUtils.createTag<WindowHandler>()

View file

@ -55,7 +55,7 @@ internal class ContentImageProvider : ImageProvider() {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) } cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) }
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) } cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) }
cursor.getColumnIndex(PATH).let { if (it != -1) fields["path"] = cursor.getString(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATA).let { if (it != -1) fields["path"] = cursor.getString(it) }
cursor.close() cursor.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -73,8 +73,5 @@ internal class ContentImageProvider : ImageProvider() {
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>() private val LOG_TAG = LogUtils.createTag<ContentImageProvider>()
@Suppress("deprecation")
const val PATH = MediaStore.MediaColumns.DATA
} }
} }

View file

@ -55,10 +55,10 @@ class MediaStoreImageProvider : ImageProvider() {
val relativePathDirectory = ensureTrailingSeparator(directory) val relativePathDirectory = ensureTrailingSeparator(directory)
val relativePath = PathSegments(context, relativePathDirectory).relativeDir val relativePath = PathSegments(context, relativePathDirectory).relativeDir
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) {
selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaColumns.PATH} LIKE ?" selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DATA} LIKE ?"
selectionArgs = arrayOf(relativePath, "$relativePathDirectory%") selectionArgs = arrayOf(relativePath, "$relativePathDirectory%")
} else { } else {
selection = "${MediaColumns.PATH} LIKE ?" selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
selectionArgs = arrayOf("$relativePathDirectory%") selectionArgs = arrayOf("$relativePathDirectory%")
} }
@ -139,12 +139,12 @@ class MediaStoreImageProvider : ImageProvider() {
fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> { fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> {
val obsoleteIds = ArrayList<Int>() val obsoleteIds = ArrayList<Int>()
fun check(context: Context, contentUri: Uri) { fun check(context: Context, contentUri: Uri) {
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaColumns.PATH) val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
try { try {
val cursor = context.contentResolver.query(contentUri, projection, null, null, null) val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
if (cursor != null) { if (cursor != null) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH) val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getInt(idColumn) val id = cursor.getInt(idColumn)
val path = cursor.getString(pathColumn) val path = cursor.getString(pathColumn)
@ -185,7 +185,7 @@ class MediaStoreImageProvider : ImageProvider() {
// image & video // image & video
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH) val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE) val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
@ -863,7 +863,7 @@ class MediaStoreImageProvider : ImageProvider() {
fun getContentUriForPath(context: Context, path: String): Uri? { fun getContentUriForPath(context: Context, path: String): Uri? {
val projection = arrayOf(MediaStore.MediaColumns._ID) val projection = arrayOf(MediaStore.MediaColumns._ID)
val selection = "${MediaColumns.PATH} = ?" val selection = "${MediaStore.MediaColumns.DATA} = ?"
val selectionArgs = arrayOf(path) val selectionArgs = arrayOf(path)
fun check(context: Context, contentUri: Uri): Uri? { fun check(context: Context, contentUri: Uri): Uri? {
@ -892,7 +892,7 @@ class MediaStoreImageProvider : ImageProvider() {
private val BASE_PROJECTION = arrayOf( private val BASE_PROJECTION = arrayOf(
MediaStore.MediaColumns._ID, MediaStore.MediaColumns._ID,
MediaColumns.PATH, MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.WIDTH,
@ -931,9 +931,6 @@ object MediaColumns {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
const val DURATION = MediaStore.MediaColumns.DURATION const val DURATION = MediaStore.MediaColumns.DURATION
@Suppress("deprecation")
const val PATH = MediaStore.MediaColumns.DATA
} }
typealias NewEntryHandler = (entry: FieldMap) -> Unit typealias NewEntryHandler = (entry: FieldMap) -> Unit

View file

@ -1,11 +1,13 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.view.Display
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? { inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -16,6 +18,14 @@ inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
} }
} }
fun Activity.getDisplayCompat(): Display? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display
} else {
@Suppress("deprecation")
windowManager.defaultDisplay
}
}
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo { fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

View file

@ -11,9 +11,9 @@ abstract class WindowService {
Future<void> requestOrientation([Orientation? orientation]); Future<void> requestOrientation([Orientation? orientation]);
Future<bool> canSetCutoutMode(); Future<bool> isCutoutAware();
Future<void> setCutoutMode(bool use); Future<EdgeInsets> getCutoutInsets();
} }
class PlatformWindowService implements WindowService { class PlatformWindowService implements WindowService {
@ -80,9 +80,9 @@ class PlatformWindowService implements WindowService {
} }
@override @override
Future<bool> canSetCutoutMode() async { Future<bool> isCutoutAware() async {
try { try {
final result = await _platform.invokeMethod('canSetCutoutMode'); final result = await _platform.invokeMethod('isCutoutAware');
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -91,13 +91,20 @@ class PlatformWindowService implements WindowService {
} }
@override @override
Future<void> setCutoutMode(bool use) async { Future<EdgeInsets> getCutoutInsets() async {
try { try {
await _platform.invokeMethod('setCutoutMode', <String, dynamic>{ final result = await _platform.invokeMethod('getCutoutInsets');
'use': use, if (result != null) {
}); return EdgeInsets.only(
left: result['left']?.toDouble() ?? 0,
top: result['top']?.toDouble() ?? 0,
right: result['right']?.toDouble() ?? 0,
bottom: result['bottom']?.toDouble() ?? 0,
);
}
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
return EdgeInsets.zero;
} }
} }

View file

@ -55,6 +55,7 @@ class AvesApp extends StatefulWidget {
// temporary exclude locales not ready yet for prime time // temporary exclude locales not ready yet for prime time
static final _unsupportedLocales = {'ar', 'fa', 'gl', 'nn', 'pl', 'th'}.map(Locale.new).toSet(); static final _unsupportedLocales = {'ar', 'fa', 'gl', 'nn', 'pl', 'th'}.map(Locale.new).toSet();
static final List<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); static final List<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList();
static final ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero);
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator');
// do not monitor all `ModalRoute`s, which would include popup menus, // do not monitor all `ModalRoute`s, which would include popup menus,
@ -164,6 +165,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
_subscriptions.add(_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?))); _subscriptions.add(_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)));
_subscriptions.add(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion())); _subscriptions.add(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()));
_subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?))); _subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?)));
_updateCutoutInsets();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
} }
@ -375,6 +377,13 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
} }
} }
@override
void didChangeMetrics() => _updateCutoutInsets();
Future<void> _updateCutoutInsets() async {
AvesApp.cutoutInsetsNotifier.value = await windowService.getCutoutInsets();
}
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
Size? _getScreenSize() { Size? _getScreenSize() {

View file

@ -143,9 +143,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
@override @override
void didChangeMetrics() { void didChangeMetrics() => _updateStatusBarHeight();
_updateStatusBarHeight();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -1,5 +1,9 @@
import 'dart:math';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -128,3 +132,59 @@ class TvTileGridBottomPaddingSliver extends StatelessWidget {
); );
} }
} }
// `MediaQuery.padding` matches cutout areas but also includes other system UI like the status bar
// so we cannot use `SafeArea` along `MediaQuery.removePadding()` to remove cutout areas
class SafeCutoutArea extends StatelessWidget {
final Animation<double>? animation;
final Widget child;
const SafeCutoutArea({
super.key,
this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<EdgeInsets>(
valueListenable: AvesApp.cutoutInsetsNotifier,
builder: (context, cutoutInsets, child) {
return ValueListenableBuilder<double>(
valueListenable: animation ?? ValueNotifier(1),
builder: (context, factor, child) {
final effectiveInsets = cutoutInsets * factor;
return Padding(
padding: effectiveInsets,
child: MediaQueryDataProvider(
value: MediaQuery.of(context).removeCutoutInsets(effectiveInsets),
child: child!,
),
);
},
child: child,
);
},
child: child,
);
}
}
extension ExtraMediaQueryData on MediaQueryData {
MediaQueryData removeCutoutInsets(EdgeInsets cutoutInsets) {
return copyWith(
padding: EdgeInsets.only(
left: max(0.0, padding.left - cutoutInsets.left),
top: max(0.0, padding.top - cutoutInsets.top),
right: max(0.0, padding.right - cutoutInsets.right),
bottom: max(0.0, padding.bottom - cutoutInsets.bottom),
),
viewPadding: EdgeInsets.only(
left: max(0.0, viewPadding.left - cutoutInsets.left),
top: max(0.0, viewPadding.top - cutoutInsets.top),
right: max(0.0, viewPadding.right - cutoutInsets.right),
bottom: max(0.0, viewPadding.bottom - cutoutInsets.bottom),
),
);
}
}

View file

@ -2,17 +2,19 @@ import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MediaQueryDataProvider extends StatelessWidget { class MediaQueryDataProvider extends StatelessWidget {
final MediaQueryData? value;
final Widget child; final Widget child;
const MediaQueryDataProvider({ const MediaQueryDataProvider({
super.key, super.key,
this.value,
required this.child, required this.child,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Provider<MediaQueryData>.value( return Provider<MediaQueryData>.value(
value: MediaQuery.of(context), value: value ?? MediaQuery.of(context),
child: child, child: child,
); );
} }

View file

@ -9,6 +9,7 @@ import 'package:aves/model/settings/enums/entry_background.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart';
import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:aves/widgets/common/fx/transition_image.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
@ -271,13 +272,20 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
image = Hero( image = Hero(
tag: widget.heroTag!, tag: widget.heroTag!,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
return TransitionImage( Widget child = TransitionImage(
image: entry.bestCachedThumbnail, image: entry.bestCachedThumbnail,
animation: animation, animation: animation,
thumbnailFit: isMosaic ? BoxFit.contain : BoxFit.cover, thumbnailFit: isMosaic ? BoxFit.contain : BoxFit.cover,
viewerFit: BoxFit.contain, viewerFit: BoxFit.contain,
background: backgroundColor, background: backgroundColor,
); );
if (!settings.viewerUseCutout) {
child = SafeCutoutArea(
animation: animation,
child: child,
);
}
return child;
}, },
transitionOnUserGestures: true, transitionOnUserGestures: true,
child: image, child: image,

View file

@ -32,13 +32,13 @@ class ViewerSection extends SettingsSection {
@override @override
FutureOr<List<SettingsTile>> tiles(BuildContext context) async { FutureOr<List<SettingsTile>> tiles(BuildContext context) async {
final canSetCutoutMode = await windowService.canSetCutoutMode(); final isCutoutAware = await windowService.isCutoutAware();
return [ return [
if (!device.isTelevision) SettingsTileViewerQuickActions(), if (!device.isTelevision) SettingsTileViewerQuickActions(),
SettingsTileViewerOverlay(), SettingsTileViewerOverlay(),
SettingsTileViewerSlideshow(), SettingsTileViewerSlideshow(),
if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(), if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(),
if (!device.isTelevision && canSetCutoutMode) SettingsTileViewerCutoutMode(), if (!device.isTelevision && isCutoutAware) SettingsTileViewerUseCutout(),
if (!device.isTelevision) SettingsTileViewerMaxBrightness(), if (!device.isTelevision) SettingsTileViewerMaxBrightness(),
SettingsTileViewerMotionPhotoAutoPlay(), SettingsTileViewerMotionPhotoAutoPlay(),
SettingsTileViewerImageBackground(), SettingsTileViewerImageBackground(),
@ -94,7 +94,7 @@ class SettingsTileViewerGestureSideTapNext extends SettingsTile {
); );
} }
class SettingsTileViewerCutoutMode extends SettingsTile { class SettingsTileViewerUseCutout extends SettingsTile {
@override @override
String title(BuildContext context) => context.l10n.settingsViewerUseCutout; String title(BuildContext context) => context.l10n.settingsViewerUseCutout;

View file

@ -95,9 +95,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!settings.viewerUseCutout) {
windowService.setCutoutMode(false);
}
if (settings.viewerMaxBrightness) { if (settings.viewerMaxBrightness) {
ScreenBrightness().setScreenBrightness(1); ScreenBrightness().setScreenBrightness(1);
} }
@ -205,88 +202,35 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
child: ValueListenableProvider<HeroInfo?>.value( child: ValueListenableProvider<HeroInfo?>.value(
value: _heroInfoNotifier, value: _heroInfoNotifier,
child: NotificationListener( child: NotificationListener(
onNotification: (dynamic notification) { onNotification: _handleNotification,
if (notification is FilterSelectedNotification) { child: LayoutBuilder(
_goToCollection(notification.filter); builder: (context, constraints) {
} else if (notification is EntryDeletedNotification) { final availableSize = Size(constraints.maxWidth, constraints.maxHeight);
_onEntryRemoved(context, notification.entries); return Stack(
} else if (notification is EntryMovedNotification) { children: [
// only add or remove entries following user actions, ViewerVerticalPageView(
// instead of applying all collection source changes collection: collection,
final isBin = collection?.filters.contains(TrashFilter.instance) ?? false; entryNotifier: entryNotifier,
final entries = notification.entries; viewerController: viewerController,
switch (notification.moveType) { overlayOpacity: _overlayInitialized
case MoveType.move: ? _overlayOpacity
_onEntryRemoved(context, entries); : settings.showOverlayOnOpening
break; ? kAlwaysCompleteAnimation
case MoveType.toBin: : kAlwaysDismissedAnimation,
if (!isBin) { verticalPager: _verticalPager,
_onEntryRemoved(context, entries); horizontalPager: _horizontalPager,
} onVerticalPageChanged: _onVerticalPageChanged,
break; onHorizontalPageChanged: _onHorizontalPageChanged,
case MoveType.fromBin: onImagePageRequested: () => _goToVerticalPage(imagePage),
if (isBin) { onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
_onEntryRemoved(context, entries); ),
} else { ..._buildOverlays(availableSize).map(_decorateOverlay),
_onEntryRestored(entries); const TopGestureAreaProtector(),
} const SideGestureAreaProtector(),
break; const BottomGestureAreaProtector(),
case MoveType.copy: ],
case MoveType.export: );
break; },
}
} else if (notification is ToggleOverlayNotification) {
_overlayVisible.value = notification.visible ?? !_overlayVisible.value;
} else if (notification is TvShowLessInfoNotification) {
if (_overlayVisible.value) {
_overlayVisible.value = false;
} else {
_onWillPop();
}
} else if (notification is TvShowMoreInfoNotification) {
if (!_overlayVisible.value) {
_overlayVisible.value = true;
}
} else if (notification is ShowInfoPageNotification) {
_goToVerticalPage(infoPage);
} else if (notification is JumpToPreviousEntryNotification) {
_jumpToHorizontalPageByDelta(-1);
} else if (notification is JumpToNextEntryNotification) {
_jumpToHorizontalPageByDelta(1);
} else if (notification is JumpToEntryNotification) {
_jumpToHorizontalPageByIndex(notification.index);
} else if (notification is VideoActionNotification) {
final controller = notification.controller;
final action = notification.action;
_onVideoAction(context, controller, action);
} else {
return false;
}
return true;
},
child: Stack(
children: [
ViewerVerticalPageView(
collection: collection,
entryNotifier: entryNotifier,
viewerController: viewerController,
overlayOpacity: _overlayInitialized
? _overlayOpacity
: settings.showOverlayOnOpening
? kAlwaysCompleteAnimation
: kAlwaysDismissedAnimation,
verticalPager: _verticalPager,
horizontalPager: _horizontalPager,
onVerticalPageChanged: _onVerticalPageChanged,
onHorizontalPageChanged: _onHorizontalPageChanged,
onImagePageRequested: () => _goToVerticalPage(imagePage),
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
),
..._buildOverlays().map(_decorateOverlay),
const TopGestureAreaProtector(),
const SideGestureAreaProtector(),
const BottomGestureAreaProtector(),
],
), ),
), ),
), ),
@ -306,46 +250,41 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
); );
} }
List<Widget> _buildOverlays() { List<Widget> _buildOverlays(Size availableSize) {
final appMode = context.read<ValueNotifier<AppMode>>().value; final appMode = context.read<ValueNotifier<AppMode>>().value;
switch (appMode) { switch (appMode) {
case AppMode.screenSaver: case AppMode.screenSaver:
return []; return [];
case AppMode.slideshow: case AppMode.slideshow:
return [ return [
_buildSlideshowBottomOverlay(), _buildSlideshowBottomOverlay(availableSize),
]; ];
default: default:
return [ return [
_buildViewerTopOverlay(), _buildViewerTopOverlay(availableSize),
_buildViewerBottomOverlay(), _buildViewerBottomOverlay(availableSize),
]; ];
} }
} }
Widget _buildSlideshowBottomOverlay() { Widget _buildSlideshowBottomOverlay(Size availableSize) {
return Selector<MediaQueryData, Size>( return SizedBox.fromSize(
selector: (context, mq) => mq.size, size: availableSize,
builder: (context, mqSize, child) { child: Align(
return SizedBox.fromSize( alignment: AlignmentDirectional.bottomEnd,
size: mqSize, child: TooltipTheme(
child: Align( data: TooltipTheme.of(context).copyWith(
alignment: AlignmentDirectional.bottomEnd, preferBelow: false,
child: TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: SlideshowButtons(
animationController: _overlayAnimationController,
),
),
), ),
); child: SlideshowButtons(
}, animationController: _overlayAnimationController,
),
),
),
); );
} }
Widget _buildViewerTopOverlay() { Widget _buildViewerTopOverlay(Size availableSize) {
Widget child = ValueListenableBuilder<AvesEntry?>( Widget child = ValueListenableBuilder<AvesEntry?>(
valueListenable: entryNotifier, valueListenable: entryNotifier,
builder: (context, mainEntry, child) { builder: (context, mainEntry, child) {
@ -359,6 +298,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
hasCollection: hasCollection, hasCollection: hasCollection,
mainEntry: mainEntry, mainEntry: mainEntry,
scale: _overlayButtonScale, scale: _overlayButtonScale,
availableSize: availableSize,
viewInsets: _frozenViewInsets, viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding, viewPadding: _frozenViewPadding,
), ),
@ -380,7 +320,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return child; return child;
} }
Widget _buildViewerBottomOverlay() { Widget _buildViewerBottomOverlay(Size availableSize) {
Widget child = ValueListenableBuilder<AvesEntry?>( Widget child = ValueListenableBuilder<AvesEntry?>(
valueListenable: entryNotifier, valueListenable: entryNotifier,
builder: (context, mainEntry, child) { builder: (context, mainEntry, child) {
@ -447,6 +387,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
index: _currentEntryIndex, index: _currentEntryIndex,
collection: collection, collection: collection,
animationController: _overlayAnimationController, animationController: _overlayAnimationController,
availableSize: availableSize,
viewInsets: _frozenViewInsets, viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding, viewPadding: _frozenViewPadding,
multiPageController: multiPageController, multiPageController: multiPageController,
@ -466,7 +407,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return AnimatedBuilder( return AnimatedBuilder(
animation: _verticalScrollNotifier, animation: _verticalScrollNotifier,
builder: (context, child) => Positioned( builder: (context, child) => Positioned(
bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight, bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - availableSize.height,
child: child!, child: child!,
), ),
child: child, child: child,
@ -478,6 +419,66 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return child; return child;
} }
bool _handleNotification(dynamic notification) {
if (notification is FilterSelectedNotification) {
_goToCollection(notification.filter);
} else if (notification is EntryDeletedNotification) {
_onEntryRemoved(context, notification.entries);
} else if (notification is EntryMovedNotification) {
// only add or remove entries following user actions,
// instead of applying all collection source changes
final isBin = collection?.filters.contains(TrashFilter.instance) ?? false;
final entries = notification.entries;
switch (notification.moveType) {
case MoveType.move:
_onEntryRemoved(context, entries);
break;
case MoveType.toBin:
if (!isBin) {
_onEntryRemoved(context, entries);
}
break;
case MoveType.fromBin:
if (isBin) {
_onEntryRemoved(context, entries);
} else {
_onEntryRestored(entries);
}
break;
case MoveType.copy:
case MoveType.export:
break;
}
} else if (notification is ToggleOverlayNotification) {
_overlayVisible.value = notification.visible ?? !_overlayVisible.value;
} else if (notification is TvShowLessInfoNotification) {
if (_overlayVisible.value) {
_overlayVisible.value = false;
} else {
_onWillPop();
}
} else if (notification is TvShowMoreInfoNotification) {
if (!_overlayVisible.value) {
_overlayVisible.value = true;
}
} else if (notification is ShowInfoPageNotification) {
_goToVerticalPage(infoPage);
} else if (notification is JumpToPreviousEntryNotification) {
_jumpToHorizontalPageByDelta(-1);
} else if (notification is JumpToNextEntryNotification) {
_jumpToHorizontalPageByDelta(1);
} else if (notification is JumpToEntryNotification) {
_jumpToHorizontalPageByIndex(notification.index);
} else if (notification is VideoActionNotification) {
final controller = notification.controller;
final action = notification.action;
_onVideoAction(context, controller, action);
} else {
return false;
}
return true;
}
Future<void> _onVideoAction(BuildContext context, AvesVideoController controller, EntryAction action) async { Future<void> _onVideoAction(BuildContext context, AvesVideoController controller, EntryAction action) async {
await _videoActionDelegate.onActionSelected(context, controller, action); await _videoActionDelegate.onActionSelected(context, controller, action);
if (action == EntryAction.videoToggleMute) { if (action == EntryAction.videoToggleMute) {
@ -673,9 +674,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
Future<void> _onLeave() async { Future<void> _onLeave() async {
if (!settings.viewerUseCutout) {
await windowService.setCutoutMode(true);
}
if (settings.viewerMaxBrightness) { if (settings.viewerMaxBrightness) {
await ScreenBrightness().resetScreenBrightness(); await ScreenBrightness().resetScreenBrightness();
} }

View file

@ -25,6 +25,7 @@ class ViewerBottomOverlay extends StatefulWidget {
final int index; final int index;
final CollectionLens? collection; final CollectionLens? collection;
final AnimationController animationController; final AnimationController animationController;
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding; final EdgeInsets? viewInsets, viewPadding;
final MultiPageController? multiPageController; final MultiPageController? multiPageController;
@ -34,6 +35,7 @@ class ViewerBottomOverlay extends StatefulWidget {
required this.index, required this.index,
required this.collection, required this.collection,
required this.animationController, required this.animationController,
required this.availableSize,
this.viewInsets, this.viewInsets,
this.viewPadding, this.viewPadding,
required this.multiPageController, required this.multiPageController,
@ -72,6 +74,7 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
mainEntry: mainEntry, mainEntry: mainEntry,
pageEntry: pageEntry ?? mainEntry, pageEntry: pageEntry ?? mainEntry,
collection: widget.collection, collection: widget.collection,
availableSize: widget.availableSize,
viewInsets: widget.viewInsets, viewInsets: widget.viewInsets,
viewPadding: widget.viewPadding, viewPadding: widget.viewPadding,
multiPageController: multiPageController, multiPageController: multiPageController,
@ -103,6 +106,7 @@ class _BottomOverlayContent extends StatefulWidget {
final int index; final int index;
final AvesEntry mainEntry, pageEntry; final AvesEntry mainEntry, pageEntry;
final CollectionLens? collection; final CollectionLens? collection;
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding; final EdgeInsets? viewInsets, viewPadding;
final MultiPageController? multiPageController; final MultiPageController? multiPageController;
final AnimationController animationController; final AnimationController animationController;
@ -113,6 +117,7 @@ class _BottomOverlayContent extends StatefulWidget {
required this.mainEntry, required this.mainEntry,
required this.pageEntry, required this.pageEntry,
required this.collection, required this.collection,
required this.availableSize,
required this.viewInsets, required this.viewInsets,
required this.viewPadding, required this.viewPadding,
required this.multiPageController, required this.multiPageController,
@ -178,89 +183,85 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
pageEntry.metadataChangeNotifier, pageEntry.metadataChangeNotifier,
]), ]),
builder: (context, child) { builder: (context, child) {
return Selector<MediaQueryData, double>( final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
selector: (context, mq) => mq.size.width, final viewerButtonRow = FocusableActionDetector(
builder: (context, mqWidth, child) { focusNode: _buttonRowFocusScopeNode,
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
final viewerButtonRow = FocusableActionDetector( actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
focusNode: _buttonRowFocusScopeNode, child: SafeArea(
shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, top: false,
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, bottom: false,
child: SafeArea( minimum: EdgeInsets.only(
top: false, left: viewInsetsPadding.left,
bottom: false, right: viewInsetsPadding.right,
minimum: EdgeInsets.only( ),
left: viewInsetsPadding.left, child: isWallpaperMode
right: viewInsetsPadding.right, ? WallpaperButtons(
entry: pageEntry,
scale: _buttonScale,
)
: ViewerButtons(
mainEntry: mainEntry,
pageEntry: pageEntry,
collection: widget.collection,
scale: _buttonScale,
),
),
);
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
final collapsedPageScroller = mainEntry.isMotionPhoto;
final availableWidth = widget.availableSize.width;
return SizedBox(
width: availableWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showMultiPageOverlay && !collapsedPageScroller)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FadeTransition(
opacity: _thumbnailOpacity,
child: MultiPageOverlay(
controller: multiPageController,
availableWidth: availableWidth,
scrollable: true,
),
),
), ),
child: isWallpaperMode (showMultiPageOverlay && collapsedPageScroller)
? WallpaperButtons( ? Row(
entry: pageEntry, crossAxisAlignment: CrossAxisAlignment.center,
scale: _buttonScale, children: [
) SafeArea(
: ViewerButtons( top: false,
mainEntry: mainEntry, bottom: false,
pageEntry: pageEntry, child: Padding(
collection: widget.collection, padding: const EdgeInsets.only(bottom: 8),
scale: _buttonScale, child: MultiPageOverlay(
), controller: multiPageController,
), availableWidth: availableWidth,
); scrollable: false,
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
final collapsedPageScroller = mainEntry.isMotionPhoto;
return SizedBox(
width: mqWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showMultiPageOverlay && !collapsedPageScroller)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FadeTransition(
opacity: _thumbnailOpacity,
child: MultiPageOverlay(
controller: multiPageController,
availableWidth: mqWidth,
scrollable: true,
),
),
),
(showMultiPageOverlay && collapsedPageScroller)
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MultiPageOverlay(
controller: multiPageController,
availableWidth: mqWidth,
scrollable: false,
),
),
), ),
Expanded(child: viewerButtonRow), ),
], ),
) Expanded(child: viewerButtonRow),
: viewerButtonRow, ],
if (settings.showOverlayThumbnailPreview && !isWallpaperMode) )
FadeTransition( : viewerButtonRow,
opacity: _thumbnailOpacity, if (settings.showOverlayThumbnailPreview && !isWallpaperMode)
child: ViewerThumbnailPreview( FadeTransition(
availableWidth: mqWidth, opacity: _thumbnailOpacity,
displayedIndex: widget.index, child: ViewerThumbnailPreview(
entries: widget.entries, availableWidth: availableWidth,
), displayedIndex: widget.index,
), entries: widget.entries,
], ),
), ),
); ],
}, ),
); );
}, },
); );

View file

@ -23,6 +23,7 @@ class ViewerDetailOverlay extends StatefulWidget {
final int index; final int index;
final bool hasCollection; final bool hasCollection;
final MultiPageController? multiPageController; final MultiPageController? multiPageController;
final Size availableSize;
const ViewerDetailOverlay({ const ViewerDetailOverlay({
super.key, super.key,
@ -30,6 +31,7 @@ class ViewerDetailOverlay extends StatefulWidget {
required this.index, required this.index,
required this.hasCollection, required this.hasCollection,
required this.multiPageController, required this.multiPageController,
required this.availableSize,
}); });
@override @override
@ -79,41 +81,35 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
return SafeArea( return SafeArea(
top: false, top: false,
bottom: false, bottom: false,
child: LayoutBuilder( child: FutureBuilder<List<dynamic>?>(
builder: (context, constraints) { future: _detailLoader,
final availableWidth = constraints.maxWidth; builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
_lastDetails = snapshot.data;
_lastEntry = entry;
}
if (_lastEntry == null) return const SizedBox();
final mainEntry = _lastEntry!;
return FutureBuilder<List<dynamic>?>( final shootingDetails = _lastDetails![0];
future: _detailLoader, final description = _lastDetails![1];
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
_lastDetails = snapshot.data;
_lastEntry = entry;
}
if (_lastEntry == null) return const SizedBox();
final mainEntry = _lastEntry!;
final shootingDetails = _lastDetails![0]; final multiPageController = widget.multiPageController;
final description = _lastDetails![1]; Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
pageEntry: pageEntry ?? mainEntry,
shootingDetails: shootingDetails,
description: description,
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
availableWidth: widget.availableSize.width,
multiPageController: multiPageController,
);
final multiPageController = widget.multiPageController; return multiPageController != null
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent( ? PageEntryBuilder(
pageEntry: pageEntry ?? mainEntry, multiPageController: multiPageController,
shootingDetails: shootingDetails, builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
description: description, )
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, : _buildContent();
availableWidth: availableWidth,
multiPageController: multiPageController,
);
return multiPageController != null
? PageEntryBuilder(
multiPageController: multiPageController,
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
)
: _buildContent();
},
);
}, },
), ),
); );

View file

@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget {
final AvesEntry mainEntry; final AvesEntry mainEntry;
final Animation<double> scale; final Animation<double> scale;
final bool hasCollection; final bool hasCollection;
final Size availableSize;
final EdgeInsets? viewInsets, viewPadding; final EdgeInsets? viewInsets, viewPadding;
const ViewerTopOverlay({ const ViewerTopOverlay({
@ -25,6 +26,7 @@ class ViewerTopOverlay extends StatelessWidget {
required this.mainEntry, required this.mainEntry,
required this.scale, required this.scale,
required this.hasCollection, required this.hasCollection,
required this.availableSize,
required this.viewInsets, required this.viewInsets,
required this.viewPadding, required this.viewPadding,
}); });
@ -65,6 +67,7 @@ class ViewerTopOverlay extends StatelessWidget {
entries: entries, entries: entries,
hasCollection: hasCollection, hasCollection: hasCollection,
multiPageController: multiPageController, multiPageController: multiPageController,
availableSize: availableSize,
), ),
), ),
), ),

View file

@ -9,6 +9,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/hero.dart';
@ -147,6 +148,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
child = _buildRasterView(); child = _buildRasterView();
} }
} }
child ??= ErrorView( child ??= ErrorView(
entry: entry, entry: entry,
onTap: _onTap, onTap: _onTap,
@ -155,6 +157,14 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
}, },
); );
if (!settings.viewerUseCutout) {
child = SafeCutoutArea(
child: ClipRect(
child: child,
),
);
}
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate); final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
if (animate) { if (animate) {
child = Consumer<HeroInfo?>( child = Consumer<HeroInfo?>(
@ -166,6 +176,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
child: child, child: child,
); );
} }
return child; return child;
} }

View file

@ -83,9 +83,6 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!settings.viewerUseCutout) {
windowService.setCutoutMode(false);
}
if (settings.viewerMaxBrightness) { if (settings.viewerMaxBrightness) {
ScreenBrightness().setScreenBrightness(1); ScreenBrightness().setScreenBrightness(1);
} }
@ -134,25 +131,30 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
} }
return true; return true;
}, },
child: Stack( child: LayoutBuilder(
children: [ builder: (context, constraints) {
SingleEntryScroller( final viewSize = Size(constraints.maxWidth, constraints.maxHeight);
entry: entry, return Stack(
viewerController: _viewerController, children: [
), SingleEntryScroller(
Positioned( entry: entry,
bottom: 0, viewerController: _viewerController,
child: _buildBottomOverlay(), ),
), Positioned(
const TopGestureAreaProtector(), bottom: 0,
const SideGestureAreaProtector(), child: _buildBottomOverlay(viewSize),
const BottomGestureAreaProtector(), ),
], const TopGestureAreaProtector(),
const SideGestureAreaProtector(),
const BottomGestureAreaProtector(),
],
);
},
), ),
); );
} }
Widget _buildBottomOverlay() { Widget _buildBottomOverlay(Size viewSize) {
final mainEntry = entry; final mainEntry = entry;
final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null; final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null;
@ -210,6 +212,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
index: 0, index: 0,
collection: null, collection: null,
animationController: _overlayAnimationController, animationController: _overlayAnimationController,
availableSize: viewSize,
viewInsets: _frozenViewInsets, viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding, viewPadding: _frozenViewPadding,
multiPageController: multiPageController, multiPageController: multiPageController,

View file

@ -119,7 +119,7 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
if (_doubleTap) { if (_doubleTap) {
// quick scale, aka one finger zoom // quick scale, aka one finger zoom
// magic numbers from `davemorrissey/subsampling-scale-image-view` // magic numbers from `davemorrissey/subsampling-scale-image-view`
final focalPointY = details.focalPoint.dy; final focalPointY = details.localFocalPoint.dy;
final distance = (focalPointY - _startFocalPoint!.dy).abs() * 2 + 20; final distance = (focalPointY - _startFocalPoint!.dy).abs() * 2 + 20;
_quickScaleLastDistance ??= distance; _quickScaleLastDistance ??= distance;
final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5; final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5;
@ -131,7 +131,7 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
} else { } else {
newScale = _startScale! * details.scale; newScale = _startScale! * details.scale;
} }
final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.focalPoint; final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.localFocalPoint;
final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!; final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!;
final scalePositionDelta = boundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1); final scalePositionDelta = boundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1);

View file

@ -17,8 +17,11 @@ class FakeWindowService extends Fake implements WindowService {
Future<void> requestOrientation([Orientation? orientation]) => SynchronousFuture(null); Future<void> requestOrientation([Orientation? orientation]) => SynchronousFuture(null);
@override @override
Future<bool> canSetCutoutMode() => SynchronousFuture(true); Future<bool> isCutoutAware() => SynchronousFuture(true);
@override @override
Future<void> setCutoutMode(bool use) => SynchronousFuture(null); Future<void> setCutoutMode(bool use) => SynchronousFuture(null);
@override
Future<EdgeInsets> getCutoutInsets() => SynchronousFuture(EdgeInsets.zero);
} }