#469 improved cutout area handling
This commit is contained in:
parent
c693055721
commit
31c14febdc
23 changed files with 415 additions and 298 deletions
|
@ -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`
|
||||
|
||||
### Fixed
|
||||
|
||||
- transition between collection and viewer when cutout area is not used
|
||||
|
||||
## <a id="v1.7.8"></a>[v1.7.8] - 2022-12-20
|
||||
|
||||
### Added
|
||||
|
|
|
@ -191,7 +191,7 @@ dependencies {
|
|||
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.14.2'
|
||||
// SLF4J implementation for `mp4parser`
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.3'
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.6'
|
||||
|
||||
// forked, built by JitPack:
|
||||
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls.window
|
|||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import deckers.thibault.aves.utils.getDisplayCompat
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
@ -42,25 +43,34 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
|
|||
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)
|
||||
}
|
||||
|
||||
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
|
||||
val use = call.argument<Boolean>("use")
|
||||
if (use == null) {
|
||||
result.error("setCutoutMode-args", "missing arguments", null)
|
||||
override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val mode = if (use) {
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
} else {
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||
}
|
||||
activity.window.attributes.layoutInDisplayCutoutMode = mode
|
||||
val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
activity.getDisplayCompat()?.cutout
|
||||
} else {
|
||||
activity.window.decorView.rootWindowInsets.displayCutout
|
||||
}
|
||||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -17,11 +17,11 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
|
|||
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)
|
||||
}
|
||||
|
||||
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(false)
|
||||
override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(HashMap<String, Any>())
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
|
|||
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
|
||||
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
|
||||
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
|
||||
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode)
|
||||
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode)
|
||||
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
|
||||
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
|
||||
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 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 {
|
||||
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
|
||||
|
|
|
@ -55,7 +55,7 @@ internal class ContentImageProvider : ImageProvider() {
|
|||
if (cursor != null && cursor.moveToFirst()) {
|
||||
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(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()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -73,8 +73,5 @@ internal class ContentImageProvider : ImageProvider() {
|
|||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>()
|
||||
|
||||
@Suppress("deprecation")
|
||||
const val PATH = MediaStore.MediaColumns.DATA
|
||||
}
|
||||
}
|
|
@ -55,10 +55,10 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val relativePathDirectory = ensureTrailingSeparator(directory)
|
||||
val relativePath = PathSegments(context, relativePathDirectory).relativeDir
|
||||
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%")
|
||||
} else {
|
||||
selection = "${MediaColumns.PATH} LIKE ?"
|
||||
selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
|
||||
selectionArgs = arrayOf("$relativePathDirectory%")
|
||||
}
|
||||
|
||||
|
@ -139,12 +139,12 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> {
|
||||
val obsoleteIds = ArrayList<Int>()
|
||||
fun check(context: Context, contentUri: Uri) {
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaColumns.PATH)
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
try {
|
||||
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
|
||||
if (cursor != null) {
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
|
||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getInt(idColumn)
|
||||
val path = cursor.getString(pathColumn)
|
||||
|
@ -185,7 +185,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
// image & video
|
||||
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 sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
|
||||
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||
|
@ -863,7 +863,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
fun getContentUriForPath(context: Context, path: String): Uri? {
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||
val selection = "${MediaColumns.PATH} = ?"
|
||||
val selection = "${MediaStore.MediaColumns.DATA} = ?"
|
||||
val selectionArgs = arrayOf(path)
|
||||
|
||||
fun check(context: Context, contentUri: Uri): Uri? {
|
||||
|
@ -892,7 +892,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
private val BASE_PROJECTION = arrayOf(
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaColumns.PATH,
|
||||
MediaStore.MediaColumns.DATA,
|
||||
MediaStore.MediaColumns.MIME_TYPE,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
|
@ -931,9 +931,6 @@ object MediaColumns {
|
|||
|
||||
@SuppressLint("InlinedApi")
|
||||
const val DURATION = MediaStore.MediaColumns.DURATION
|
||||
|
||||
@Suppress("deprecation")
|
||||
const val PATH = MediaStore.MediaColumns.DATA
|
||||
}
|
||||
|
||||
typealias NewEntryHandler = (entry: FieldMap) -> Unit
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.view.Display
|
||||
|
||||
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
|
||||
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 {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
|
|
@ -11,9 +11,9 @@ abstract class WindowService {
|
|||
|
||||
Future<void> requestOrientation([Orientation? orientation]);
|
||||
|
||||
Future<bool> canSetCutoutMode();
|
||||
Future<bool> isCutoutAware();
|
||||
|
||||
Future<void> setCutoutMode(bool use);
|
||||
Future<EdgeInsets> getCutoutInsets();
|
||||
}
|
||||
|
||||
class PlatformWindowService implements WindowService {
|
||||
|
@ -80,9 +80,9 @@ class PlatformWindowService implements WindowService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<bool> canSetCutoutMode() async {
|
||||
Future<bool> isCutoutAware() async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('canSetCutoutMode');
|
||||
final result = await _platform.invokeMethod('isCutoutAware');
|
||||
if (result != null) return result as bool;
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
|
@ -91,13 +91,20 @@ class PlatformWindowService implements WindowService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> setCutoutMode(bool use) async {
|
||||
Future<EdgeInsets> getCutoutInsets() async {
|
||||
try {
|
||||
await _platform.invokeMethod('setCutoutMode', <String, dynamic>{
|
||||
'use': use,
|
||||
});
|
||||
final result = await _platform.invokeMethod('getCutoutInsets');
|
||||
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) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return EdgeInsets.zero;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ class AvesApp extends StatefulWidget {
|
|||
// temporary exclude locales not ready yet for prime time
|
||||
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 ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero);
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||
|
||||
// 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(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()));
|
||||
_subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?)));
|
||||
_updateCutoutInsets();
|
||||
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();
|
||||
|
||||
Size? _getScreenSize() {
|
||||
|
|
|
@ -143,9 +143,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
_updateStatusBarHeight();
|
||||
}
|
||||
void didChangeMetrics() => _updateStatusBarHeight();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import 'dart:math';
|
||||
|
||||
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/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:flutter/material.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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,19 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
class MediaQueryDataProvider extends StatelessWidget {
|
||||
final MediaQueryData? value;
|
||||
final Widget child;
|
||||
|
||||
const MediaQueryDataProvider({
|
||||
super.key,
|
||||
this.value,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider<MediaQueryData>.value(
|
||||
value: MediaQuery.of(context),
|
||||
value: value ?? MediaQuery.of(context),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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/settings.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/transition_image.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
|
@ -271,13 +272,20 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
|||
image = Hero(
|
||||
tag: widget.heroTag!,
|
||||
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
|
||||
return TransitionImage(
|
||||
Widget child = TransitionImage(
|
||||
image: entry.bestCachedThumbnail,
|
||||
animation: animation,
|
||||
thumbnailFit: isMosaic ? BoxFit.contain : BoxFit.cover,
|
||||
viewerFit: BoxFit.contain,
|
||||
background: backgroundColor,
|
||||
);
|
||||
if (!settings.viewerUseCutout) {
|
||||
child = SafeCutoutArea(
|
||||
animation: animation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
transitionOnUserGestures: true,
|
||||
child: image,
|
||||
|
|
|
@ -32,13 +32,13 @@ class ViewerSection extends SettingsSection {
|
|||
|
||||
@override
|
||||
FutureOr<List<SettingsTile>> tiles(BuildContext context) async {
|
||||
final canSetCutoutMode = await windowService.canSetCutoutMode();
|
||||
final isCutoutAware = await windowService.isCutoutAware();
|
||||
return [
|
||||
if (!device.isTelevision) SettingsTileViewerQuickActions(),
|
||||
SettingsTileViewerOverlay(),
|
||||
SettingsTileViewerSlideshow(),
|
||||
if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(),
|
||||
if (!device.isTelevision && canSetCutoutMode) SettingsTileViewerCutoutMode(),
|
||||
if (!device.isTelevision && isCutoutAware) SettingsTileViewerUseCutout(),
|
||||
if (!device.isTelevision) SettingsTileViewerMaxBrightness(),
|
||||
SettingsTileViewerMotionPhotoAutoPlay(),
|
||||
SettingsTileViewerImageBackground(),
|
||||
|
@ -94,7 +94,7 @@ class SettingsTileViewerGestureSideTapNext extends SettingsTile {
|
|||
);
|
||||
}
|
||||
|
||||
class SettingsTileViewerCutoutMode extends SettingsTile {
|
||||
class SettingsTileViewerUseCutout extends SettingsTile {
|
||||
@override
|
||||
String title(BuildContext context) => context.l10n.settingsViewerUseCutout;
|
||||
|
||||
|
|
|
@ -95,9 +95,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!settings.viewerUseCutout) {
|
||||
windowService.setCutoutMode(false);
|
||||
}
|
||||
if (settings.viewerMaxBrightness) {
|
||||
ScreenBrightness().setScreenBrightness(1);
|
||||
}
|
||||
|
@ -205,88 +202,35 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
child: ValueListenableProvider<HeroInfo?>.value(
|
||||
value: _heroInfoNotifier,
|
||||
child: NotificationListener(
|
||||
onNotification: (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;
|
||||
},
|
||||
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(),
|
||||
],
|
||||
onNotification: _handleNotification,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableSize = Size(constraints.maxWidth, constraints.maxHeight);
|
||||
return 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(availableSize).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;
|
||||
switch (appMode) {
|
||||
case AppMode.screenSaver:
|
||||
return [];
|
||||
case AppMode.slideshow:
|
||||
return [
|
||||
_buildSlideshowBottomOverlay(),
|
||||
_buildSlideshowBottomOverlay(availableSize),
|
||||
];
|
||||
default:
|
||||
return [
|
||||
_buildViewerTopOverlay(),
|
||||
_buildViewerBottomOverlay(),
|
||||
_buildViewerTopOverlay(availableSize),
|
||||
_buildViewerBottomOverlay(availableSize),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSlideshowBottomOverlay() {
|
||||
return Selector<MediaQueryData, Size>(
|
||||
selector: (context, mq) => mq.size,
|
||||
builder: (context, mqSize, child) {
|
||||
return SizedBox.fromSize(
|
||||
size: mqSize,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
child: SlideshowButtons(
|
||||
animationController: _overlayAnimationController,
|
||||
),
|
||||
),
|
||||
Widget _buildSlideshowBottomOverlay(Size availableSize) {
|
||||
return SizedBox.fromSize(
|
||||
size: availableSize,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SlideshowButtons(
|
||||
animationController: _overlayAnimationController,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildViewerTopOverlay() {
|
||||
Widget _buildViewerTopOverlay(Size availableSize) {
|
||||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||
valueListenable: entryNotifier,
|
||||
builder: (context, mainEntry, child) {
|
||||
|
@ -359,6 +298,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
hasCollection: hasCollection,
|
||||
mainEntry: mainEntry,
|
||||
scale: _overlayButtonScale,
|
||||
availableSize: availableSize,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
),
|
||||
|
@ -380,7 +320,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
return child;
|
||||
}
|
||||
|
||||
Widget _buildViewerBottomOverlay() {
|
||||
Widget _buildViewerBottomOverlay(Size availableSize) {
|
||||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||
valueListenable: entryNotifier,
|
||||
builder: (context, mainEntry, child) {
|
||||
|
@ -447,6 +387,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
index: _currentEntryIndex,
|
||||
collection: collection,
|
||||
animationController: _overlayAnimationController,
|
||||
availableSize: availableSize,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
multiPageController: multiPageController,
|
||||
|
@ -466,7 +407,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
return AnimatedBuilder(
|
||||
animation: _verticalScrollNotifier,
|
||||
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,
|
||||
|
@ -478,6 +419,66 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
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 {
|
||||
await _videoActionDelegate.onActionSelected(context, controller, action);
|
||||
if (action == EntryAction.videoToggleMute) {
|
||||
|
@ -673,9 +674,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
}
|
||||
|
||||
Future<void> _onLeave() async {
|
||||
if (!settings.viewerUseCutout) {
|
||||
await windowService.setCutoutMode(true);
|
||||
}
|
||||
if (settings.viewerMaxBrightness) {
|
||||
await ScreenBrightness().resetScreenBrightness();
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ class ViewerBottomOverlay extends StatefulWidget {
|
|||
final int index;
|
||||
final CollectionLens? collection;
|
||||
final AnimationController animationController;
|
||||
final Size availableSize;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final MultiPageController? multiPageController;
|
||||
|
||||
|
@ -34,6 +35,7 @@ class ViewerBottomOverlay extends StatefulWidget {
|
|||
required this.index,
|
||||
required this.collection,
|
||||
required this.animationController,
|
||||
required this.availableSize,
|
||||
this.viewInsets,
|
||||
this.viewPadding,
|
||||
required this.multiPageController,
|
||||
|
@ -72,6 +74,7 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
|||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
collection: widget.collection,
|
||||
availableSize: widget.availableSize,
|
||||
viewInsets: widget.viewInsets,
|
||||
viewPadding: widget.viewPadding,
|
||||
multiPageController: multiPageController,
|
||||
|
@ -103,6 +106,7 @@ class _BottomOverlayContent extends StatefulWidget {
|
|||
final int index;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
final CollectionLens? collection;
|
||||
final Size availableSize;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final MultiPageController? multiPageController;
|
||||
final AnimationController animationController;
|
||||
|
@ -113,6 +117,7 @@ class _BottomOverlayContent extends StatefulWidget {
|
|||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
required this.collection,
|
||||
required this.availableSize,
|
||||
required this.viewInsets,
|
||||
required this.viewPadding,
|
||||
required this.multiPageController,
|
||||
|
@ -178,89 +183,85 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
|||
pageEntry.metadataChangeNotifier,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
return Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.size.width,
|
||||
builder: (context, mqWidth, child) {
|
||||
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||
final viewerButtonRow = FocusableActionDetector(
|
||||
focusNode: _buttonRowFocusScopeNode,
|
||||
shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
|
||||
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
minimum: EdgeInsets.only(
|
||||
left: viewInsetsPadding.left,
|
||||
right: viewInsetsPadding.right,
|
||||
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||
final viewerButtonRow = FocusableActionDetector(
|
||||
focusNode: _buttonRowFocusScopeNode,
|
||||
shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
|
||||
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
minimum: EdgeInsets.only(
|
||||
left: viewInsetsPadding.left,
|
||||
right: viewInsetsPadding.right,
|
||||
),
|
||||
child: isWallpaperMode
|
||||
? 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
|
||||
? 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;
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
(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: availableWidth,
|
||||
scrollable: false,
|
||||
),
|
||||
Expanded(child: viewerButtonRow),
|
||||
],
|
||||
)
|
||||
: viewerButtonRow,
|
||||
if (settings.showOverlayThumbnailPreview && !isWallpaperMode)
|
||||
FadeTransition(
|
||||
opacity: _thumbnailOpacity,
|
||||
child: ViewerThumbnailPreview(
|
||||
availableWidth: mqWidth,
|
||||
displayedIndex: widget.index,
|
||||
entries: widget.entries,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(child: viewerButtonRow),
|
||||
],
|
||||
)
|
||||
: viewerButtonRow,
|
||||
if (settings.showOverlayThumbnailPreview && !isWallpaperMode)
|
||||
FadeTransition(
|
||||
opacity: _thumbnailOpacity,
|
||||
child: ViewerThumbnailPreview(
|
||||
availableWidth: availableWidth,
|
||||
displayedIndex: widget.index,
|
||||
entries: widget.entries,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -23,6 +23,7 @@ class ViewerDetailOverlay extends StatefulWidget {
|
|||
final int index;
|
||||
final bool hasCollection;
|
||||
final MultiPageController? multiPageController;
|
||||
final Size availableSize;
|
||||
|
||||
const ViewerDetailOverlay({
|
||||
super.key,
|
||||
|
@ -30,6 +31,7 @@ class ViewerDetailOverlay extends StatefulWidget {
|
|||
required this.index,
|
||||
required this.hasCollection,
|
||||
required this.multiPageController,
|
||||
required this.availableSize,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -79,41 +81,35 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
|||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
child: FutureBuilder<List<dynamic>?>(
|
||||
future: _detailLoader,
|
||||
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>?>(
|
||||
future: _detailLoader,
|
||||
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 description = _lastDetails![1];
|
||||
|
||||
final shootingDetails = _lastDetails![0];
|
||||
final description = _lastDetails![1];
|
||||
final multiPageController = widget.multiPageController;
|
||||
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;
|
||||
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
shootingDetails: shootingDetails,
|
||||
description: description,
|
||||
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
||||
availableWidth: availableWidth,
|
||||
multiPageController: multiPageController,
|
||||
);
|
||||
|
||||
return multiPageController != null
|
||||
? PageEntryBuilder(
|
||||
multiPageController: multiPageController,
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent();
|
||||
},
|
||||
);
|
||||
return multiPageController != null
|
||||
? PageEntryBuilder(
|
||||
multiPageController: multiPageController,
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
final AvesEntry mainEntry;
|
||||
final Animation<double> scale;
|
||||
final bool hasCollection;
|
||||
final Size availableSize;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
|
||||
const ViewerTopOverlay({
|
||||
|
@ -25,6 +26,7 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
required this.mainEntry,
|
||||
required this.scale,
|
||||
required this.hasCollection,
|
||||
required this.availableSize,
|
||||
required this.viewInsets,
|
||||
required this.viewPadding,
|
||||
});
|
||||
|
@ -65,6 +67,7 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
entries: entries,
|
||||
hasCollection: hasCollection,
|
||||
multiPageController: multiPageController,
|
||||
availableSize: availableSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.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/viewer/controller.dart';
|
||||
import 'package:aves/widgets/viewer/hero.dart';
|
||||
|
@ -147,6 +148,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
|||
child = _buildRasterView();
|
||||
}
|
||||
}
|
||||
|
||||
child ??= ErrorView(
|
||||
entry: entry,
|
||||
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);
|
||||
if (animate) {
|
||||
child = Consumer<HeroInfo?>(
|
||||
|
@ -166,6 +176,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
|||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
|
|
|
@ -83,9 +83,6 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!settings.viewerUseCutout) {
|
||||
windowService.setCutoutMode(false);
|
||||
}
|
||||
if (settings.viewerMaxBrightness) {
|
||||
ScreenBrightness().setScreenBrightness(1);
|
||||
}
|
||||
|
@ -134,25 +131,30 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
}
|
||||
return true;
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleEntryScroller(
|
||||
entry: entry,
|
||||
viewerController: _viewerController,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: _buildBottomOverlay(),
|
||||
),
|
||||
const TopGestureAreaProtector(),
|
||||
const SideGestureAreaProtector(),
|
||||
const BottomGestureAreaProtector(),
|
||||
],
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final viewSize = Size(constraints.maxWidth, constraints.maxHeight);
|
||||
return Stack(
|
||||
children: [
|
||||
SingleEntryScroller(
|
||||
entry: entry,
|
||||
viewerController: _viewerController,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: _buildBottomOverlay(viewSize),
|
||||
),
|
||||
const TopGestureAreaProtector(),
|
||||
const SideGestureAreaProtector(),
|
||||
const BottomGestureAreaProtector(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomOverlay() {
|
||||
Widget _buildBottomOverlay(Size viewSize) {
|
||||
final mainEntry = entry;
|
||||
final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null;
|
||||
|
||||
|
@ -210,6 +212,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
index: 0,
|
||||
collection: null,
|
||||
animationController: _overlayAnimationController,
|
||||
availableSize: viewSize,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
multiPageController: multiPageController,
|
||||
|
|
|
@ -119,7 +119,7 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
|||
if (_doubleTap) {
|
||||
// quick scale, aka one finger zoom
|
||||
// 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;
|
||||
_quickScaleLastDistance ??= distance;
|
||||
final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5;
|
||||
|
@ -131,7 +131,7 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
|||
} else {
|
||||
newScale = _startScale! * details.scale;
|
||||
}
|
||||
final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.focalPoint;
|
||||
final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.localFocalPoint;
|
||||
|
||||
final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!;
|
||||
final scalePositionDelta = boundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1);
|
||||
|
|
|
@ -17,8 +17,11 @@ class FakeWindowService extends Fake implements WindowService {
|
|||
Future<void> requestOrientation([Orientation? orientation]) => SynchronousFuture(null);
|
||||
|
||||
@override
|
||||
Future<bool> canSetCutoutMode() => SynchronousFuture(true);
|
||||
Future<bool> isCutoutAware() => SynchronousFuture(true);
|
||||
|
||||
@override
|
||||
Future<void> setCutoutMode(bool use) => SynchronousFuture(null);
|
||||
|
||||
@override
|
||||
Future<EdgeInsets> getCutoutInsets() => SynchronousFuture(EdgeInsets.zero);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue