#275 system bar transparency review

This commit is contained in:
Thibault Deckers 2022-08-09 12:36:27 +02:00
parent b29425e322
commit 73e9073407
22 changed files with 124 additions and 90 deletions

View file

@ -40,7 +40,7 @@ All notable changes to this project will be documented in this file.
- slideshow - slideshow
- set wallpaper from any media - set wallpaper from any media
- optional dynamic accent color on Android 12+ - optional dynamic accent color on Android >=12
- Search: date/dimension/size field equality (undocumented) - Search: date/dimension/size field equality (undocumented)
- support Android 13 (API 33) - support Android 13 (API 33)
- Turkish translation (thanks metezd) - Turkish translation (thanks metezd)
@ -135,7 +135,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- app launch despite faulty storage volumes on Android 11+ - app launch despite faulty storage volumes on Android >=11
## <a id="v1.6.2"></a>[v1.6.2] - 2022-03-07 ## <a id="v1.6.2"></a>[v1.6.2] - 2022-03-07
@ -320,7 +320,7 @@ All notable changes to this project will be documented in this file.
- Info: improved display for PNG text metadata, XMP and others - Info: improved display for PNG text metadata, XMP and others
- Export: output format selection - Export: output format selection
- Search: added raw filter - Search: added raw filter
- Support modifying files in the Download folder on Android 11+ - Support modifying files in the Download folder on Android >=11
### Changed ### Changed
@ -330,7 +330,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- hide root album of hidden path - hide root album of hidden path
- gesture & spacing handling for Android 10+ navigation gestures - gesture & spacing handling for Android >=10 navigation gestures
- renaming was leaving behind obsolete items in some cases - renaming was leaving behind obsolete items in some cases
- speeding up videos on Xiaomi devices - speeding up videos on Xiaomi devices
@ -393,7 +393,7 @@ All notable changes to this project will be documented in this file.
- Map & Stats from selection - Map & Stats from selection
- Map: item browsing, rotation control - Map: item browsing, rotation control
- Navigation menu customization - Navigation menu customization
- shortcut support on older devices (API < 26) - shortcut support on older devices (API <26)
- support Android 12/S (API 31) - support Android 12/S (API 31)
## [v1.4.8] - 2021-08-08 ## [v1.4.8] - 2021-08-08
@ -407,7 +407,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- auto album identification and naming - auto album identification and naming
- opening HEIC images from downloads content URI on Android R+ - opening HEIC images from downloads content URI on Android >=11
## [v1.4.7] - 2021-08-06 [YANKED] ## [v1.4.7] - 2021-08-06 [YANKED]
@ -611,7 +611,7 @@ All notable changes to this project will be documented in this file.
- Viewer: support for multi-track HEIF - Viewer: support for multi-track HEIF
- Viewer: export image (including multipage TIFF/HEIF and images embedded in XMP) - Viewer: export image (including multipage TIFF/HEIF and images embedded in XMP)
- Info: show owner app (Android Q and up) - Info: show owner app (Android >=10)
- listen to Media Store changes - listen to Media Store changes
### Changed ### Changed
@ -638,7 +638,7 @@ upgraded libtiff to 4.2.0 for TIFF decoding
### Fixed ### Fixed
- prevent scrolling when using Android Q style gesture navigation - prevent scrolling when using Android 10 style gesture navigation
## [v1.3.1] - 2021-01-04 ## [v1.3.1] - 2021-01-04

View file

@ -12,8 +12,8 @@ This change eventually prevents building the app with Flutter v3.0.2.
android:installLocation="auto"> android:installLocation="auto">
<!-- <!--
Scoped storage on Android Q is inconvenient because users need to confirm edition on each individual file. Scoped storage on Android 10 is inconvenient because users need to confirm edition on each individual file.
So we request `WRITE_EXTERNAL_STORAGE` until Q (29), and enable `requestLegacyExternalStorage` So we request `WRITE_EXTERNAL_STORAGE` until Android 10 (API 29), and enable `requestLegacyExternalStorage`
--> -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
@ -31,25 +31,25 @@ This change eventually prevents building the app with Flutter v3.0.2.
<!-- to show foreground service progress via notification --> <!-- to show foreground service progress via notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- to access media with original metadata with scoped storage (Android Q+) --> <!-- to access media with original metadata with scoped storage (Android >=10) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- TODO TLAD remove this permission when this is fixed: https://github.com/flutter/flutter/issues/42451 --> <!-- TODO TLAD remove this permission when this is fixed: https://github.com/flutter/flutter/issues/42451 -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- for API < 26 --> <!-- for API <26 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<!-- allow install on API 19, but Google Maps is from API 20 --> <!-- allow install on API 19, but Google Maps is from API 20 -->
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps" /> <uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps" />
<!-- from Android R, we should define <queries> to make other apps visible to this app --> <!-- from Android 11, we should define <queries> to make other apps visible to this app -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
</intent> </intent>
<!-- <!--
from Android R, `url_launcher` method `canLaunchUrl()` will return false, from Android 11, `url_launcher` method `canLaunchUrl()` will return false,
if appropriate intents are not declared, cf https://pub.dev/packages/url_launcher#configuration= if appropriate intents are not declared, cf https://pub.dev/packages/url_launcher#configuration=
--> -->
<!-- to open https URLs --> <!-- to open https URLs -->

View file

@ -285,7 +285,7 @@ open class MainActivity : FlutterActivity() {
val filters = intent.getStringArrayExtra(EXTRA_KEY_FILTERS_ARRAY)?.toList() val filters = intent.getStringArrayExtra(EXTRA_KEY_FILTERS_ARRAY)?.toList()
if (filters != null) return filters if (filters != null) return filters
// fallback for shortcuts created on API < 26 // fallback for shortcuts created on API <26
val filterString = intent.getStringExtra(EXTRA_KEY_FILTERS_STRING) val filterString = intent.getStringExtra(EXTRA_KEY_FILTERS_STRING)
if (filterString != null) { if (filterString != null) {
return filterString.split(EXTRA_STRING_ARRAY_SEPARATOR) return filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)

View file

@ -56,7 +56,7 @@ class ThumbnailFetcher internal constructor(
try { try {
if (!customFetch && (width == defaultSize || height == defaultSize) && !isFlipped) { if (!customFetch && (width == defaultSize || height == defaultSize) && !isFlipped) {
// Fetch low quality thumbnails when size is not specified. // Fetch low quality thumbnails when size is not specified.
// As of Android R, the Media Store content resolver may return a thumbnail // As of Android 11, the Media Store content resolver may return a thumbnail
// that is automatically rotated according to EXIF orientation, but not flipped, // that is automatically rotated according to EXIF orientation, but not flipped,
// so we skip this step for flipped entries. // so we skip this step for flipped entries.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -108,7 +108,7 @@ class ThumbnailFetcher internal constructor(
} else { } else {
@Suppress("deprecation") @Suppress("deprecation")
var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null) var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null)
// from Android Q, returned thumbnail is already rotated according to EXIF orientation // from Android 10, returned thumbnail is already rotated according to EXIF orientation
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
} }

View file

@ -3,7 +3,6 @@ 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.LogUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -60,8 +59,4 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
} }
result.success(true) result.success(true)
} }
companion object {
private val LOG_TAG = LogUtils.createTag<ActivityWindowHandler>()
}
} }

View file

@ -88,7 +88,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
error("requestMediaFileAccess-unsupported", "media file bulk access is not allowed before Android R", null) error("requestMediaFileAccess-unsupported", "media file bulk access is not allowed before Android 11", null)
return return
} }

View file

@ -193,7 +193,7 @@ class MediaStoreImageProvider : ImageProvider() {
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN) val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
// image & video for API >= Q, only for images for API < Q // image & video for API >=29, only for images for API <29
val orientationColumn = cursor.getColumnIndex(MediaColumns.ORIENTATION) val orientationColumn = cursor.getColumnIndex(MediaColumns.ORIENTATION)
// video only // video only
@ -347,7 +347,7 @@ class MediaStoreImageProvider : ImageProvider() {
} }
} catch (securityException: SecurityException) { } catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory, // even if the app has access permission granted on the containing directory,
// the delete request may yield a `RecoverableSecurityException` on Android 10+ // the delete request may yield a `RecoverableSecurityException` on Android >=10
// when the underlying file no longer exists and this is an orphaned entry in the Media Store // when the underlying file no longer exists and this is an orphaned entry in the Media Store
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) {
Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException) Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException)
@ -876,7 +876,7 @@ class MediaStoreImageProvider : ImageProvider() {
private val VIDEO_PROJECTION = arrayOf( private val VIDEO_PROJECTION = arrayOf(
*BASE_PROJECTION, *BASE_PROJECTION,
MediaColumns.DURATION, MediaColumns.DURATION,
// `ORIENTATION` was only available for images before Android Q // `ORIENTATION` was only available for images before Android 10
*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf( *if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(
MediaStore.MediaColumns.ORIENTATION, MediaStore.MediaColumns.ORIENTATION,
) else emptyArray() ) else emptyArray()

View file

@ -2,7 +2,7 @@ package deckers.thibault.aves.utils
import android.os.Build import android.os.Build
// compatibility extension for `removeIf` for API < N // compatibility extension for `removeIf` for API <24
fun <E> MutableList<E>.compatRemoveIf(filter: (t: E) -> Boolean): Boolean { fun <E> MutableList<E>.compatRemoveIf(filter: (t: E) -> Boolean): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.removeIf(filter) this.removeIf(filter)

View file

@ -98,7 +98,7 @@ object PermissionManager {
segments.volumePath?.let { volumePath -> segments.volumePath?.let { volumePath ->
val dirSet = dirsPerVolume[volumePath] ?: HashSet() val dirSet = dirsPerVolume[volumePath] ?: HashSet()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// request primary directory on volume from Android R // request primary directory on volume from Android 11
val relativeDir = segments.relativeDir val relativeDir = segments.relativeDir
if (relativeDir != null) { if (relativeDir != null) {
val dirSegments = relativeDir.split(File.separator).takeWhile { it.isNotEmpty() } val dirSegments = relativeDir.split(File.separator).takeWhile { it.isNotEmpty() }
@ -111,11 +111,11 @@ object PermissionManager {
} }
} else { } else {
// the requested path is the volume root itself // the requested path is the volume root itself
// which cannot be granted, due to Android R restrictions // which cannot be granted, due to Android 11 restrictions
dirSet.add("") dirSet.add("")
} }
} else { } else {
// request volume root until Android Q // request volume root until Android 10
dirSet.add("") dirSet.add("")
} }
dirsPerVolume[volumePath] = dirSet dirsPerVolume[volumePath] = dirSet
@ -236,7 +236,7 @@ object PermissionManager {
return dirs return dirs
} }
// As of Android R, `MediaStore.getDocumentUri` fails if any of the persisted // As of Android 11, `MediaStore.getDocumentUri` fails if any of the persisted
// URI permissions we hold points to a folder that no longer exists, // URI permissions we hold points to a folder that no longer exists,
// so we should remove these obsolete URIs before proceeding. // so we should remove these obsolete URIs before proceeding.
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)

View file

@ -468,7 +468,7 @@ object StorageUtils {
fun requireAccessPermission(context: Context, anyPath: String): Boolean { fun requireAccessPermission(context: Context, anyPath: String): Boolean {
if (isAppFile(context, anyPath)) return false if (isAppFile(context, anyPath)) return false
// on Android R, we should always require access permission, even on primary volume // on Android 11, we should always require access permission, even on primary volume
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
val onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath(context)) val onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath(context))
@ -487,7 +487,7 @@ object StorageUtils {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) {
val path = uri.path val path = uri.path
path ?: return uri path ?: return uri
// from Android R, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException` // from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException`
if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) { if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) {
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original" // "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
@ -499,7 +499,7 @@ object StorageUtils {
} }
// As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used // As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used
// to work around a bug from Android Q where metadata redaction corrupts HEIC images. // to work around a bug from Android 10 where metadata redaction corrupts HEIC images.
// This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException` // This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException`
// for some non image/video content URIs (e.g. `downloads`, `file`) // for some non image/video content URIs (e.g. `downloads`, `file`)
fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String): Uri { fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String): Uri {
@ -594,7 +594,7 @@ object StorageUtils {
val effectiveUri = getOriginalUri(context, uri) val effectiveUri = getOriginalUri(context, uri)
return try { return try {
MediaMetadataRetriever().apply { MediaMetadataRetriever().apply {
// on Android S preview, setting the data source works but yields an internal IOException // on Android 12 preview, setting the data source works but yields an internal IOException
// (`Input file descriptor already original`), whether we provide the original URI or not // (`Input file descriptor already original`), whether we provide the original URI or not
setDataSource(context, effectiveUri) setDataSource(context, effectiveUri)
} }

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="translucentNavBar">false</bool>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<!-- API28+, draws next to the notch in fullscreen -->
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="28">shortEdges</item>
</style>
</resources>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowTranslucentNavigation">@bool/translucentNavBar</item> <!-- API19+, tinted background & request the SYSTEM_UI_FLAG_LAYOUT_STABLE and SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN flags -->
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> <!-- API28+, draws next to the notch in fullscreen -->
</style>
</resources>

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="translucentNavBar">true</bool>
</resources>

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowTranslucentNavigation">@bool/translucentNavBar</item> <!-- API19+, tinted background & request the SYSTEM_UI_FLAG_LAYOUT_STABLE and SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN flags --> <item name="android:windowBackground">?android:colorBackground</item>
<!-- API28+, draws next to the notch in fullscreen -->
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="28">shortEdges</item>
</style> </style>
</resources> </resources>

View file

@ -2,8 +2,8 @@ import 'dart:ui';
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/widgets/aves_app.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class Themes { class Themes {
@ -35,7 +35,7 @@ class Themes {
static const _lightSecondLayer = Color(0xFFF5F5F5); // aka `Colors.grey[100]` static const _lightSecondLayer = Color(0xFFF5F5F5); // aka `Colors.grey[100]`
static const _lightThirdLayer = Color(0xFFEEEEEE); // aka `Colors.grey[200]` static const _lightThirdLayer = Color(0xFFEEEEEE); // aka `Colors.grey[200]`
static ThemeData lightTheme(Color accentColor) => ThemeData( static ThemeData lightTheme(Color accentColor, bool deviceInitialized) => ThemeData(
colorScheme: ColorScheme.light( colorScheme: ColorScheme.light(
primary: accentColor, primary: accentColor,
secondary: accentColor, secondary: accentColor,
@ -58,7 +58,7 @@ class Themes {
foregroundColor: _lightActionIconColor, foregroundColor: _lightActionIconColor,
// `titleTextStyle.color` is used by text // `titleTextStyle.color` is used by text
titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor), titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor),
systemOverlayStyle: SystemUiOverlayStyle.dark, systemOverlayStyle: deviceInitialized ? AvesApp.systemUIStyleForBrightness(Brightness.light, _lightFirstLayer) : null,
), ),
listTileTheme: const ListTileThemeData( listTileTheme: const ListTileThemeData(
iconColor: _lightActionIconColor, iconColor: _lightActionIconColor,
@ -87,7 +87,7 @@ class Themes {
static const _darkSecondLayer = Color(0xFF363636); static const _darkSecondLayer = Color(0xFF363636);
static const _darkThirdLayer = Color(0xFF424242); // aka `Colors.grey[800]` static const _darkThirdLayer = Color(0xFF424242); // aka `Colors.grey[800]`
static ThemeData darkTheme(Color accentColor) => ThemeData( static ThemeData darkTheme(Color accentColor, bool deviceInitialized) => ThemeData(
colorScheme: ColorScheme.dark( colorScheme: ColorScheme.dark(
primary: accentColor, primary: accentColor,
secondary: accentColor, secondary: accentColor,
@ -112,7 +112,7 @@ class Themes {
foregroundColor: _darkTitleColor, foregroundColor: _darkTitleColor,
// `titleTextStyle.color` is used by text // `titleTextStyle.color` is used by text
titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor), titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor),
systemOverlayStyle: SystemUiOverlayStyle.light, systemOverlayStyle: deviceInitialized ? AvesApp.systemUIStyleForBrightness(Brightness.dark, _darkFirstLayer) : null,
), ),
popupMenuTheme: const PopupMenuThemeData( popupMenuTheme: const PopupMenuThemeData(
color: _darkSecondLayer, color: _darkSecondLayer,
@ -138,8 +138,8 @@ class Themes {
static const _blackSecondLayer = Color(0xFF212121); // aka `Colors.grey[900]` static const _blackSecondLayer = Color(0xFF212121); // aka `Colors.grey[900]`
static const _blackThirdLayer = Color(0xFF303030); // aka `Colors.grey[850]` static const _blackThirdLayer = Color(0xFF303030); // aka `Colors.grey[850]`
static ThemeData blackTheme(Color accentColor) { static ThemeData blackTheme(Color accentColor, bool deviceInitialized) {
final baseTheme = darkTheme(accentColor); final baseTheme = darkTheme(accentColor, deviceInitialized);
return baseTheme.copyWith( return baseTheme.copyWith(
// `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard`
canvasColor: _blackSecondLayer, canvasColor: _blackSecondLayer,

View file

@ -60,15 +60,44 @@ class AvesApp extends StatefulWidget {
@override @override
State<AvesApp> createState() => _AvesAppState(); State<AvesApp> createState() => _AvesAppState();
static void showSystemUI() { static void setSystemUIStyle(BuildContext context) {
final theme = Theme.of(context);
final style = systemUIStyleForBrightness(theme.brightness, theme.scaffoldBackgroundColor);
SystemChrome.setSystemUIOverlayStyle(style);
}
static SystemUiOverlayStyle systemUIStyleForBrightness(Brightness themeBrightness, Color scaffoldBackgroundColor) {
final barBrightness = themeBrightness == Brightness.light ? Brightness.dark : Brightness.light;
const statusBarColor = Colors.transparent;
// as of Flutter v3.3.0-0.2.pre, setting `SystemUiOverlayStyle` (whether manually or automatically because of `AppBar`)
// prevents the canvas from drawing behind the nav bar on Android <10 (API <29),
// so the nav bar is opaque, even when requesting `SystemUiMode.edgeToEdge` from Flutter
// or setting `android:windowTranslucentNavigation` in Android themes.
final navBarColor = device.supportEdgeToEdgeUIMode ? Colors.transparent : scaffoldBackgroundColor;
return SystemUiOverlayStyle(
systemNavigationBarColor: navBarColor,
systemNavigationBarDividerColor: navBarColor,
systemNavigationBarIconBrightness: barBrightness,
// shows background scrim when using navigation buttons, but not when using gesture navigation
systemNavigationBarContrastEnforced: true,
statusBarColor: statusBarColor,
statusBarBrightness: barBrightness,
statusBarIconBrightness: barBrightness,
systemStatusBarContrastEnforced: false,
);
}
static Future<void> showSystemUI() async {
if (device.supportEdgeToEdgeUIMode) { if (device.supportEdgeToEdgeUIMode) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else { } else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
} }
} }
static void hideSystemUI() => SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); static Future<void> hideSystemUI() async {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
} }
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@ -121,6 +150,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
future: _appSetup, future: _appSetup,
builder: (context, snapshot) { builder: (context, snapshot) {
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
if (initialized) {
AvesApp.showSystemUI();
}
final home = initialized final home = initialized
? getFirstPage() ? getFirstPage()
: Scaffold( : Scaffold(
@ -169,17 +201,22 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
navigatorKey: AvesApp.navigatorKey, navigatorKey: AvesApp.navigatorKey,
home: home, home: home,
navigatorObservers: _navigatorObservers, navigatorObservers: _navigatorObservers,
builder: (context, child) => AvesColorsProvider( builder: (context, child) {
child: Theme( if (initialized) {
data: Theme.of(context).copyWith( WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(context));
pageTransitionsTheme: pageTransitionsTheme, }
return AvesColorsProvider(
child: Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: pageTransitionsTheme,
),
child: child!,
), ),
child: child!, );
), },
),
onGenerateTitle: (context) => context.l10n.appName, onGenerateTitle: (context) => context.l10n.appName,
theme: Themes.lightTheme(lightAccent), theme: Themes.lightTheme(lightAccent, initialized),
darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent) : Themes.darkTheme(darkAccent), darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized),
themeMode: themeBrightness.appThemeMode, themeMode: themeBrightness.appThemeMode,
locale: settingsLocale, locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,

View file

@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
// This widget should be added on top of Scaffolds with: // This widget should be added on top of Scaffolds with:
// - `resizeToAvoidBottomInset` set to false, // - `resizeToAvoidBottomInset` set to false,
// - a vertically scrollable body. // - a vertically scrollable body.
// It will prevent the body from scrolling when a user swipe from bottom to use Android Q style navigation gestures. // It will prevent the body from scrolling when a user swipe from bottom to use Android 10 style navigation gestures.
class BottomGestureAreaProtector extends StatelessWidget { class BottomGestureAreaProtector extends StatelessWidget {
const BottomGestureAreaProtector({super.key}); const BottomGestureAreaProtector({super.key});
@ -45,7 +45,7 @@ class TopGestureAreaProtector extends StatelessWidget {
} }
} }
// It will prevent the body from scrolling when a user swipe from edges to use Android Q style navigation gestures. // It will prevent the body from scrolling when a user swipe from edges to use Android 10 style navigation gestures.
class SideGestureAreaProtector extends StatelessWidget { class SideGestureAreaProtector extends StatelessWidget {
const SideGestureAreaProtector({super.key}); const SideGestureAreaProtector({super.key});

View file

@ -95,7 +95,7 @@ class _HomePageState extends State<HomePage> {
// hide in some countries apps that force quit on permission denial // hide in some countries apps that force quit on permission denial
await [ await [
Permission.storage, Permission.storage,
// to access media with unredacted metadata with scoped storage (Android 10+) // to access media with unredacted metadata with scoped storage (Android >=10)
Permission.accessMediaLocation, Permission.accessMediaLocation,
].request(); ].request();
} }

View file

@ -652,19 +652,20 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
} }
void _onLeave() { Future<void> _onLeave() async {
if (!settings.viewerUseCutout) { if (!settings.viewerUseCutout) {
windowService.setCutoutMode(true); await windowService.setCutoutMode(true);
} }
if (settings.viewerMaxBrightness) { if (settings.viewerMaxBrightness) {
ScreenBrightness().resetScreenBrightness(); await ScreenBrightness().resetScreenBrightness();
} }
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
windowService.keepScreenOn(false); await windowService.keepScreenOn(false);
} }
AvesApp.showSystemUI(); await AvesApp.showSystemUI();
windowService.requestOrientation(); AvesApp.setSystemUIStyle(context);
await windowService.requestOrientation();
} }
// overlay // overlay
@ -679,7 +680,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
Future<void> _onOverlayVisibleChange({bool animate = true}) async { Future<void> _onOverlayVisibleChange({bool animate = true}) async {
if (_overlayVisible.value) { if (_overlayVisible.value) {
AvesApp.showSystemUI(); await AvesApp.showSystemUI();
AvesApp.setSystemUIStyle(context);
if (animate) { if (animate) {
await _overlayAnimationController.forward(); await _overlayAnimationController.forward();
} else { } else {
@ -692,7 +694,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
_frozenViewInsets = mediaQuery.viewInsets; _frozenViewInsets = mediaQuery.viewInsets;
_frozenViewPadding = mediaQuery.viewPadding; _frozenViewPadding = mediaQuery.viewPadding;
}); });
AvesApp.hideSystemUI(); await AvesApp.hideSystemUI();
if (animate) { if (animate) {
await _overlayAnimationController.reverse(); await _overlayAnimationController.reverse();
} else { } else {

View file

@ -159,8 +159,9 @@ class _PanoramaPageState extends State<PanoramaPage> {
} }
} }
void _onLeave() { Future<void> _onLeave() async {
AvesApp.showSystemUI(); await AvesApp.showSystemUI();
AvesApp.setSystemUIStyle(context);
} }
// system UI // system UI
@ -176,9 +177,10 @@ class _PanoramaPageState extends State<PanoramaPage> {
Future<void> _onOverlayVisibleChange() async { Future<void> _onOverlayVisibleChange() async {
if (_overlayVisible.value) { if (_overlayVisible.value) {
AvesApp.showSystemUI(); await AvesApp.showSystemUI();
AvesApp.setSystemUIStyle(context);
} else { } else {
AvesApp.hideSystemUI(); await AvesApp.hideSystemUI();
} }
} }
} }

View file

@ -234,7 +234,8 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
Future<void> _onOverlayVisibleChange({bool animate = true}) async { Future<void> _onOverlayVisibleChange({bool animate = true}) async {
if (_overlayVisible.value) { if (_overlayVisible.value) {
AvesApp.showSystemUI(); await AvesApp.showSystemUI();
AvesApp.setSystemUIStyle(context);
if (animate) { if (animate) {
await _overlayAnimationController.forward(); await _overlayAnimationController.forward();
} else { } else {
@ -246,7 +247,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
_frozenViewInsets = mediaQuery.viewInsets; _frozenViewInsets = mediaQuery.viewInsets;
_frozenViewPadding = mediaQuery.viewPadding; _frozenViewPadding = mediaQuery.viewPadding;
}); });
AvesApp.hideSystemUI(); await AvesApp.hideSystemUI();
if (animate) { if (animate) {
await _overlayAnimationController.reverse(); await _overlayAnimationController.reverse();
} else { } else {