#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`
|
- 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
activity.window.decorView.rootWindowInsets.displayCutout
|
||||||
}
|
}
|
||||||
activity.window.attributes.layoutInDisplayCutoutMode = mode
|
if (cutout == null) {
|
||||||
|
result.error("getCutoutInsets-null", "cutout insets are null", null)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
result.success(true)
|
|
||||||
|
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)
|
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>())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,66 +202,11 @@ 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) {
|
|
||||||
// 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: [
|
children: [
|
||||||
ViewerVerticalPageView(
|
ViewerVerticalPageView(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
|
@ -282,11 +224,13 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
||||||
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
|
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
|
||||||
),
|
),
|
||||||
..._buildOverlays().map(_decorateOverlay),
|
..._buildOverlays(availableSize).map(_decorateOverlay),
|
||||||
const TopGestureAreaProtector(),
|
const TopGestureAreaProtector(),
|
||||||
const SideGestureAreaProtector(),
|
const SideGestureAreaProtector(),
|
||||||
const BottomGestureAreaProtector(),
|
const BottomGestureAreaProtector(),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -306,29 +250,26 @@ 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>(
|
|
||||||
selector: (context, mq) => mq.size,
|
|
||||||
builder: (context, mqSize, child) {
|
|
||||||
return SizedBox.fromSize(
|
return SizedBox.fromSize(
|
||||||
size: mqSize,
|
size: availableSize,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
child: TooltipTheme(
|
child: TooltipTheme(
|
||||||
|
@ -341,11 +282,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,9 +183,6 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
pageEntry.metadataChangeNotifier,
|
pageEntry.metadataChangeNotifier,
|
||||||
]),
|
]),
|
||||||
builder: (context, child) {
|
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 viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||||
final viewerButtonRow = FocusableActionDetector(
|
final viewerButtonRow = FocusableActionDetector(
|
||||||
focusNode: _buttonRowFocusScopeNode,
|
focusNode: _buttonRowFocusScopeNode,
|
||||||
|
@ -210,8 +212,9 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
|
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
|
||||||
final collapsedPageScroller = mainEntry.isMotionPhoto;
|
final collapsedPageScroller = mainEntry.isMotionPhoto;
|
||||||
|
|
||||||
|
final availableWidth = widget.availableSize.width;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: mqWidth,
|
width: availableWidth,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
@ -223,7 +226,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
opacity: _thumbnailOpacity,
|
opacity: _thumbnailOpacity,
|
||||||
child: MultiPageOverlay(
|
child: MultiPageOverlay(
|
||||||
controller: multiPageController,
|
controller: multiPageController,
|
||||||
availableWidth: mqWidth,
|
availableWidth: availableWidth,
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -239,7 +242,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: MultiPageOverlay(
|
child: MultiPageOverlay(
|
||||||
controller: multiPageController,
|
controller: multiPageController,
|
||||||
availableWidth: mqWidth,
|
availableWidth: availableWidth,
|
||||||
scrollable: false,
|
scrollable: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -252,7 +255,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
FadeTransition(
|
FadeTransition(
|
||||||
opacity: _thumbnailOpacity,
|
opacity: _thumbnailOpacity,
|
||||||
child: ViewerThumbnailPreview(
|
child: ViewerThumbnailPreview(
|
||||||
availableWidth: mqWidth,
|
availableWidth: availableWidth,
|
||||||
displayedIndex: widget.index,
|
displayedIndex: widget.index,
|
||||||
entries: widget.entries,
|
entries: widget.entries,
|
||||||
),
|
),
|
||||||
|
@ -262,8 +265,6 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||||
|
|
|
@ -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,11 +81,7 @@ 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) {
|
|
||||||
final availableWidth = constraints.maxWidth;
|
|
||||||
|
|
||||||
return FutureBuilder<List<dynamic>?>(
|
|
||||||
future: _detailLoader,
|
future: _detailLoader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||||
|
@ -102,7 +100,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
shootingDetails: shootingDetails,
|
shootingDetails: shootingDetails,
|
||||||
description: description,
|
description: description,
|
||||||
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
||||||
availableWidth: availableWidth,
|
availableWidth: widget.availableSize.width,
|
||||||
multiPageController: multiPageController,
|
multiPageController: multiPageController,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -113,8 +111,6 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
)
|
)
|
||||||
: _buildContent();
|
: _buildContent();
|
||||||
},
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,7 +131,10 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final viewSize = Size(constraints.maxWidth, constraints.maxHeight);
|
||||||
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
SingleEntryScroller(
|
SingleEntryScroller(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
@ -142,17 +142,19 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: _buildBottomOverlay(),
|
child: _buildBottomOverlay(viewSize),
|
||||||
),
|
),
|
||||||
const TopGestureAreaProtector(),
|
const TopGestureAreaProtector(),
|
||||||
const SideGestureAreaProtector(),
|
const SideGestureAreaProtector(),
|
||||||
const BottomGestureAreaProtector(),
|
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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue