Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2023-03-13 12:01:10 +01:00
commit 7b2f72cf14
153 changed files with 2414 additions and 1369 deletions

@ -1 +1 @@
Subproject commit c07f7888888435fd9df505aa2efc38d3cf65681b Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da

View file

@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.8.3"></a>[v1.8.3] - 2023-03-13
### Added
- Collection: preview button when selecting items
- Collection: item size in list layout
- Vaults: custom pattern lock
- Video: picture-in-picture
- Video: handle skip next/previous media buttons
- TV: more media controls
### Changed
- scroll to show item when navigating from Info page
- upgraded Flutter to stable v3.7.7
### Fixed
- Accessibility: using accessibility services keeping snack bar beyond countdown
- Accessibility: navigation with TalkBack
- Vaults: crash when using fingerprint on older Android versions
- Vaults: sharing multiple items
## <a id="v1.8.2"></a>[v1.8.2] - 2023-02-28 ## <a id="v1.8.2"></a>[v1.8.2] - 2023-02-28
### Added ### Added

View file

@ -67,6 +67,16 @@ This change eventually prevents building the app with Flutter v3.3.3.
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
</intent> </intent>
<!-- necessary to resolve image editor apps that are not visible in the launcher -->
<intent>
<action android:name="android.intent.action.EDIT" />
<data android:mimeType="image/*" />
</intent>
<!-- necessary to resolve video editor apps that are not visible in the launcher -->
<intent>
<action android:name="android.intent.action.EDIT" />
<data android:mimeType="video/*" />
</intent>
<!-- <!--
from Android 11, `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=
@ -96,6 +106,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:theme="@style/NormalTheme" android:theme="@style/NormalTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
@ -283,7 +294,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- as of Flutter v3.3.0, background blur & icon shading fail with Impeller --> <!-- as of Flutter v3.7.7, background blur yields black screen with Impeller -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.EnableImpeller" android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" /> android:value="false" />

View file

@ -13,17 +13,20 @@ import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterFragmentActivity() { class WallpaperActivity : FlutterFragmentActivity() {
private lateinit var intentDataMap: MutableMap<String, Any?> private lateinit var intentDataMap: MutableMap<String, Any?>
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (FlutterUtils.isSoftwareRenderingRequired()) { if (FlutterUtils.isSoftwareRenderingRequired()) {
@ -42,12 +45,19 @@ class WallpaperActivity : FlutterFragmentActivity() {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart // dart -> platform -> dart
// - need Context // - need Context
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this)) MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this)) MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper // - need ContextWrapper
@ -79,6 +89,11 @@ class WallpaperActivity : FlutterFragmentActivity() {
} }
} }
override fun onDestroy() {
mediaSessionHandler.dispose()
super.onDestroy()
}
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getIntentData" -> { "getIntentData" -> {

View file

@ -285,7 +285,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
return return
} }
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) }) val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { getShareableUri(context, Uri.parse(it)) })
val mimeTypes = urisByMimeType.keys.toTypedArray() val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more // simplify share intent for a single item, as some apps can handle one item but not more
@ -296,7 +296,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
Intent(Intent.ACTION_SEND) Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType) .setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri)) .putExtra(Intent.EXTRA_STREAM, uri)
} else { } else {
var mimeType = "*/*" var mimeType = "*/*"
if (mimeTypes.size == 1) { if (mimeTypes.size == 1) {

View file

@ -65,11 +65,13 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
val stateString = call.argument<String>("state") val stateString = call.argument<String>("state")
val positionMillis = call.argument<Number>("positionMillis")?.toLong() val positionMillis = call.argument<Number>("positionMillis")?.toLong()
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat() val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
val canSkipToNext = call.argument<Boolean>("canSkipToNext")
val canSkipToPrevious = call.argument<Boolean>("canSkipToPrevious")
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) { if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null || canSkipToNext == null || canSkipToPrevious == null) {
result.error( result.error(
"updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" + "updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" +
", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed", null ", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed, canSkipToNext=$canSkipToNext, canSkipToPrevious=$canSkipToPrevious", null
) )
return return
} }
@ -90,6 +92,12 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
} else { } else {
actions or PlaybackStateCompat.ACTION_PLAY actions or PlaybackStateCompat.ACTION_PLAY
} }
if (canSkipToNext) {
actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
}
if (canSkipToPrevious) {
actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
}
val playbackState = PlaybackStateCompat.Builder() val playbackState = PlaybackStateCompat.Builder()
.setState( .setState(

View file

@ -46,6 +46,16 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE)) success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE))
} }
override fun onSkipToNext() {
super.onSkipToNext()
success(hashMapOf(KEY_COMMAND to COMMAND_SKIP_TO_NEXT))
}
override fun onSkipToPrevious() {
super.onSkipToPrevious()
success(hashMapOf(KEY_COMMAND to COMMAND_SKIP_TO_PREVIOUS))
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
success(hashMapOf(KEY_COMMAND to COMMAND_STOP)) success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
@ -70,6 +80,8 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
const val COMMAND_PLAY = "play" const val COMMAND_PLAY = "play"
const val COMMAND_PAUSE = "pause" const val COMMAND_PAUSE = "pause"
const val COMMAND_SKIP_TO_NEXT = "skip_to_next"
const val COMMAND_SKIP_TO_PREVIOUS = "skip_to_previous"
const val COMMAND_STOP = "stop" const val COMMAND_STOP = "stop"
const val COMMAND_SEEK = "seek" const val COMMAND_SEEK = "seek"
} }

View file

@ -44,8 +44,14 @@ object StorageUtils {
const val TRASH_PATH_PLACEHOLDER = "#trash" const val TRASH_PATH_PLACEHOLDER = "#trash"
// whether the provided path is on one of this app specific directories:
// - /storage/{volume}/Android/data/{package_name}/files
// - /data/user/0/{package_name}/files
private fun isAppFile(context: Context, path: String): Boolean { private fun isAppFile(context: Context, path: String): Boolean {
val dirs = context.getExternalFilesDirs(null).filterNotNull() val dirs = listOf(
*context.getExternalFilesDirs(null).filterNotNull().toTypedArray(),
context.filesDir,
)
return dirs.any { path.startsWith(it.path) } return dirs.any { path.startsWith(it.path) }
} }

View file

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

View file

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

View file

@ -0,0 +1,5 @@
In v1.8.3:
- view items in full-screen when selecting them
- watch videos using picture-in-picture
- navigate with TalkBack
Full changelog available on GitHub

View file

@ -0,0 +1,5 @@
In v1.8.3:
- view items in full-screen when selecting them
- watch videos using picture-in-picture
- navigate with TalkBack
Full changelog available on GitHub

View file

@ -2,4 +2,4 @@
<b>Навигация и поиск</b> важные части <i>Aves</i>. Пользователи могут легко переходить от альбомов к фотографиям, тэгам, картам и т.д. <b>Навигация и поиск</b> важные части <i>Aves</i>. Пользователи могут легко переходить от альбомов к фотографиям, тэгам, картам и т.д.
<i>Aves</i> интегрируется с Android (начиная с версии <b>API 19 до 33</b>, т.е. от KitKat до Android 13) предлагая такие возможности как <b>виджеты</b>, <b>пользовательские ярлыки</b>, <b>скринсейвер</b> и поддержку <b>глобального поиска</b>. Он так же работает как диалоговое окно для <b>просмотра и выбора медиа</b>. <i>Aves</i> интегрируется с Android (от KitKat до Android 13, включая Android TV) предлагая такие возможности как <b>виджеты</b>, <b>пользовательские ярлыки</b>, <b>скринсейвер</b> и поддержку <b>глобального поиска</b>. Он так же работает как диалоговое окно для <b>просмотра и выбора медиа</b>.

View file

@ -19,6 +19,11 @@ extension ExtraAppMode on AppMode {
AppMode.pickMultipleMediaExternal, AppMode.pickMultipleMediaExternal,
}.contains(this); }.contains(this);
bool get canEditEntry => {
AppMode.main,
AppMode.view,
}.contains(this);
bool get canSelectMedia => { bool get canSelectMedia => {
AppMode.main, AppMode.main,
AppMode.pickMultipleMediaExternal, AppMode.pickMultipleMediaExternal,

View file

@ -1366,5 +1366,65 @@
"settingsViewerShowDescription": "Zobrazit popis", "settingsViewerShowDescription": "Zobrazit popis",
"@settingsViewerShowDescription": {}, "@settingsViewerShowDescription": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti", "settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti",
"@settingsVideoGestureVerticalDragBrightnessVolume": {} "@settingsVideoGestureVerticalDragBrightnessVolume": {},
"settingsDisablingBinWarningDialogMessage": "Položky v koši budou navždy odstraněny.",
"@settingsDisablingBinWarningDialogMessage": {},
"chipActionLock": "Uzamknout",
"@chipActionLock": {},
"chipActionGoToPlacePage": "Zobrazit v místech",
"@chipActionGoToPlacePage": {},
"vaultLockTypePassword": "Heslo",
"@vaultLockTypePassword": {},
"settingsConfirmationVaultDataLoss": "Zobrazit varování o možnosti ztráty dat trezorů",
"@settingsConfirmationVaultDataLoss": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"albumTierVaults": "Trezory",
"@albumTierVaults": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"chipActionCreateVault": "Vytvořit trezor",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Upravit trezor",
"@chipActionConfigureVault": {},
"newVaultWarningDialogMessage": "Položky v trezorech jsou přístupné pouze této aplikaci a žádné jiné.\n\nPokud tuto aplikaci odinstalujete, nebo smažete její data, o všechny tyto položky přijdete.",
"@newVaultWarningDialogMessage": {},
"vaultLockTypePattern": "Vzor",
"@vaultLockTypePattern": {},
"vaultDialogLockTypeLabel": "Typ uzamčení",
"@vaultDialogLockTypeLabel": {},
"pinDialogEnter": "Zadejte PIN",
"@pinDialogEnter": {},
"patternDialogConfirm": "Potvrďte gesto",
"@patternDialogConfirm": {},
"patternDialogEnter": "Zadejte gesto",
"@patternDialogEnter": {},
"pinDialogConfirm": "Potvrďte PIN",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Zadejte heslo",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Potvrďte heslo",
"@passwordDialogConfirm": {},
"exportEntryDialogWriteMetadata": "Zapsat metadata",
"@exportEntryDialogWriteMetadata": {},
"drawerPlacePage": "Místa",
"@drawerPlacePage": {},
"placePageTitle": "Místa",
"@placePageTitle": {},
"placeEmpty": "Žádná místa",
"@placeEmpty": {},
"authenticateToConfigureVault": "Pro úpravu teroru se ověřte",
"@authenticateToConfigureVault": {},
"authenticateToUnlockVault": "Ověřte se pro otevření trezoru",
"@authenticateToUnlockVault": {},
"newVaultDialogTitle": "Nový trezor",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Nastavit trezor",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultBinUsageDialogMessage": "Některé trezory používají koš.",
"@vaultBinUsageDialogMessage": {}
} }

View file

@ -226,9 +226,12 @@
"unitSystemMetric": "Metric", "unitSystemMetric": "Metric",
"unitSystemImperial": "Imperial", "unitSystemImperial": "Imperial",
"vaultLockTypePin": "Pin", "vaultLockTypePattern": "Pattern",
"vaultLockTypePin": "PIN",
"vaultLockTypePassword": "Password", "vaultLockTypePassword": "Password",
"settingsVideoEnablePip": "Picture-in-picture",
"videoControlsPlay": "Play", "videoControlsPlay": "Play",
"videoControlsPlaySeek": "Play & seek backward/forward", "videoControlsPlaySeek": "Play & seek backward/forward",
"videoControlsPlayOutside": "Open with other player", "videoControlsPlayOutside": "Open with other player",
@ -384,8 +387,11 @@
"vaultDialogLockModeWhenScreenOff": "Lock when screen turns off", "vaultDialogLockModeWhenScreenOff": "Lock when screen turns off",
"vaultDialogLockTypeLabel": "Lock type", "vaultDialogLockTypeLabel": "Lock type",
"pinDialogEnter": "Enter pin", "patternDialogEnter": "Enter pattern",
"pinDialogConfirm": "Confirm pin", "patternDialogConfirm": "Confirm pattern",
"pinDialogEnter": "Enter PIN",
"pinDialogConfirm": "Confirm PIN",
"passwordDialogEnter": "Enter password", "passwordDialogEnter": "Enter password",
"passwordDialogConfirm": "Confirm password", "passwordDialogConfirm": "Confirm password",
@ -792,6 +798,8 @@
"settingsVideoAutoPlay": "Auto play", "settingsVideoAutoPlay": "Auto play",
"settingsVideoLoopModeTile": "Loop mode", "settingsVideoLoopModeTile": "Loop mode",
"settingsVideoLoopModeDialogTitle": "Loop Mode", "settingsVideoLoopModeDialogTitle": "Loop Mode",
"settingsVideoBackgroundMode": "Background mode",
"settingsVideoBackgroundModeDialogTitle": "Background Mode",
"settingsSubtitleThemeTile": "Subtitles", "settingsSubtitleThemeTile": "Subtitles",
"settingsSubtitleThemePageTitle": "Subtitles", "settingsSubtitleThemePageTitle": "Subtitles",

View file

@ -1262,5 +1262,17 @@
"lengthUnitPercent": "%", "lengthUnitPercent": "%",
"@lengthUnitPercent": {}, "@lengthUnitPercent": {},
"exportEntryDialogWriteMetadata": "Escribir metadatos", "exportEntryDialogWriteMetadata": "Escribir metadatos",
"@exportEntryDialogWriteMetadata": {} "@exportEntryDialogWriteMetadata": {},
"vaultLockTypePattern": "Patrón",
"@vaultLockTypePattern": {},
"patternDialogEnter": "Introduzca el patrón",
"@patternDialogEnter": {},
"patternDialogConfirm": "Confirme el patrón",
"@patternDialogConfirm": {},
"settingsVideoEnablePip": "Imagen-en-imagen",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Reproducción de fondo",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Background mode",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1420,5 +1420,17 @@
"vaultLockTypePassword": "Pasahitza", "vaultLockTypePassword": "Pasahitza",
"@vaultLockTypePassword": {}, "@vaultLockTypePassword": {},
"drawerPlacePage": "Lekuak", "drawerPlacePage": "Lekuak",
"@drawerPlacePage": {} "@drawerPlacePage": {},
"vaultLockTypePattern": "Patroia",
"@vaultLockTypePattern": {},
"patternDialogEnter": "Sartu patroia",
"@patternDialogEnter": {},
"patternDialogConfirm": "Konfirmatu patroia",
"@patternDialogConfirm": {},
"settingsVideoEnablePip": "Bideoa leihotxoan",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1262,5 +1262,17 @@
"lengthUnitPercent": "%", "lengthUnitPercent": "%",
"@lengthUnitPercent": {}, "@lengthUnitPercent": {},
"exportEntryDialogWriteMetadata": "Écrire les métadonnées", "exportEntryDialogWriteMetadata": "Écrire les métadonnées",
"@exportEntryDialogWriteMetadata": {} "@exportEntryDialogWriteMetadata": {},
"patternDialogEnter": "Entrez votre modèle",
"@patternDialogEnter": {},
"patternDialogConfirm": "Confirmez votre modèle",
"@patternDialogConfirm": {},
"vaultLockTypePattern": "Modèle",
"@vaultLockTypePattern": {},
"settingsVideoEnablePip": "Picture-in-picture",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Lecture en arrière-plan",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Arrière-plan",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1262,5 +1262,17 @@
"lengthUnitPixel": "px", "lengthUnitPixel": "px",
"@lengthUnitPixel": {}, "@lengthUnitPixel": {},
"lengthUnitPercent": "%", "lengthUnitPercent": "%",
"@lengthUnitPercent": {} "@lengthUnitPercent": {},
"vaultLockTypePattern": "Pola",
"@vaultLockTypePattern": {},
"patternDialogConfirm": "Konfirmasi pola",
"@patternDialogConfirm": {},
"patternDialogEnter": "Masukkan pola",
"@patternDialogEnter": {},
"settingsVideoEnablePip": "Gambar dalam gambar",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Mode latar belakang",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Mode Latar Belakang",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1262,5 +1262,17 @@
"lengthUnitPercent": "%", "lengthUnitPercent": "%",
"@lengthUnitPercent": {}, "@lengthUnitPercent": {},
"exportEntryDialogWriteMetadata": "메타데이터 저장", "exportEntryDialogWriteMetadata": "메타데이터 저장",
"@exportEntryDialogWriteMetadata": {} "@exportEntryDialogWriteMetadata": {},
"patternDialogConfirm": "패턴을 확인하세요",
"@patternDialogConfirm": {},
"patternDialogEnter": "패턴을 입력하세요",
"@patternDialogEnter": {},
"vaultLockTypePattern": "패턴",
"@vaultLockTypePattern": {},
"settingsVideoEnablePip": "PIP (화면 속 화면)",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "백그라운드 재생",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1366,5 +1366,59 @@
"tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.", "tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke", "settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke",
"@settingsVideoGestureVerticalDragBrightnessVolume": {} "@settingsVideoGestureVerticalDragBrightnessVolume": {},
"settingsDisablingBinWarningDialogMessage": "Elementer i papirkurven vil bli slettet for godt.",
"@settingsDisablingBinWarningDialogMessage": {},
"chipActionLock": "Lås",
"@chipActionLock": {},
"chipActionGoToPlacePage": "Vis i «Steder»",
"@chipActionGoToPlacePage": {},
"chipActionCreateVault": "Opprett hvelv",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Sett opp hvelv",
"@chipActionConfigureVault": {},
"albumTierVaults": "Hvelv",
"@albumTierVaults": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"newVaultDialogTitle": "Nytt hvelv",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Sett opp hvelv",
"@configureVaultDialogTitle": {},
"pinDialogEnter": "Skriv inn PIN",
"@pinDialogEnter": {},
"pinDialogConfirm": "Bekreft PIN",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Skriv inn passord",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Bekreft passord",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Identitetsbekreft for å sette opp hvelv",
"@authenticateToConfigureVault": {},
"authenticateToUnlockVault": "Identitetsbekreft for å låse opp hvelv",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Noen hvelv bruker papirkurven.",
"@vaultBinUsageDialogMessage": {},
"newVaultWarningDialogMessage": "Elementer i hvelv er kun tilgjengelig for dette programmet, og ikke andre.\n\nHvis du avinstallerer dette programmet, eller tømmer denne programdataen vil du miste alle disse elementene.",
"@newVaultWarningDialogMessage": {},
"drawerPlacePage": "Steder",
"@drawerPlacePage": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "Passord",
"@vaultLockTypePassword": {},
"exportEntryDialogWriteMetadata": "Skriv metadata",
"@exportEntryDialogWriteMetadata": {},
"placeEmpty": "Ingen steder",
"@placeEmpty": {},
"vaultDialogLockModeWhenScreenOff": "Lås når skjermen skrur seg av",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Låsetype",
"@vaultDialogLockTypeLabel": {},
"placePageTitle": "Steder",
"@placePageTitle": {},
"settingsConfirmationVaultDataLoss": "Vis advarsel om hvelv-datatap",
"@settingsConfirmationVaultDataLoss": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {}
} }

View file

@ -1420,5 +1420,17 @@
"lengthUnitPercent": "%", "lengthUnitPercent": "%",
"@lengthUnitPercent": {}, "@lengthUnitPercent": {},
"placePageTitle": "Miejsca", "placePageTitle": "Miejsca",
"@placePageTitle": {} "@placePageTitle": {},
"vaultLockTypePattern": "Wzór",
"@vaultLockTypePattern": {},
"patternDialogEnter": "Ustaw wzór",
"@patternDialogEnter": {},
"patternDialogConfirm": "Potwierdź wzór",
"@patternDialogConfirm": {},
"settingsVideoEnablePip": "Obraz w obrazie",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Tryb tła",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Tryb tła",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1216,5 +1216,61 @@
"pinDialogConfirm": "Confirme o PIN", "pinDialogConfirm": "Confirme o PIN",
"@pinDialogConfirm": {}, "@pinDialogConfirm": {},
"passwordDialogEnter": "Digite a senha", "passwordDialogEnter": "Digite a senha",
"@passwordDialogEnter": {} "@passwordDialogEnter": {},
"columnCount": "{count, plural, =1{1 coluna} other{{count} colunas}}",
"@columnCount": {
"placeholders": {
"count": {}
}
},
"settingsDisablingBinWarningDialogMessage": "Os itens na lixeira serão excluídos para sempre.",
"@settingsDisablingBinWarningDialogMessage": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"chipActionCreateVault": "Criar cofre",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Configurar cofre",
"@chipActionConfigureVault": {},
"albumTierVaults": "Cofres",
"@albumTierVaults": {},
"lengthUnitPixel": "pixels",
"@lengthUnitPixel": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"exportEntryDialogWriteMetadata": "Escrever metadados",
"@exportEntryDialogWriteMetadata": {},
"chipActionGoToPlacePage": "Exibir em Lugares",
"@chipActionGoToPlacePage": {},
"placePageTitle": "Lugares",
"@placePageTitle": {},
"drawerPlacePage": "Lugares",
"@drawerPlacePage": {},
"placeEmpty": "Sem lugares",
"@placeEmpty": {},
"tooManyItemsErrorDialogMessage": "Tente novamente com menos itens.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Deslize para cima ou para baixo para ajustar o brilho/volume",
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"passwordDialogConfirm": "Confirmar senha",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Autentique-se para configurar o cofre",
"@authenticateToConfigureVault": {},
"authenticateToUnlockVault": "Autentique-se para desbloquear o cofre",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Alguns cofres estão usando a lixeira.",
"@vaultBinUsageDialogMessage": {},
"newVaultWarningDialogMessage": "Os itens nos cofres estão disponíveis apenas para este aplicativo e nenhum outro.\n\nSe você desinstalar este aplicativo ou limpar os dados do aplicativo, perderá todos esses itens.",
"@newVaultWarningDialogMessage": {},
"settingsConfirmationVaultDataLoss": "Mostrar aviso de perda de dados do cofre",
"@settingsConfirmationVaultDataLoss": {},
"patternDialogEnter": "Inserir padrão",
"@patternDialogEnter": {},
"patternDialogConfirm": "Confirmar padrão",
"@patternDialogConfirm": {},
"vaultLockTypePattern": "Padrão",
"@vaultLockTypePattern": {},
"settingsVideoEnablePip": "Picture-in-picture",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Modo background",
"@settingsVideoBackgroundMode": {}
} }

View file

@ -1230,5 +1230,19 @@
"albumTierVaults": "Хранилища", "albumTierVaults": "Хранилища",
"@albumTierVaults": {}, "@albumTierVaults": {},
"newVaultWarningDialogMessage": "Элементы внутри хранилищ доступны только для этого приложения, и никакого другого.\n\nЕсли вы удалите приложение или очистите его данные, то вы потеряете все содержимое внутри хранилищ.", "newVaultWarningDialogMessage": "Элементы внутри хранилищ доступны только для этого приложения, и никакого другого.\n\nЕсли вы удалите приложение или очистите его данные, то вы потеряете все содержимое внутри хранилищ.",
"@newVaultWarningDialogMessage": {} "@newVaultWarningDialogMessage": {},
"filterLocatedLabel": "С местоположением",
"@filterLocatedLabel": {},
"filterTaggedLabel": "С тэгами",
"@filterTaggedLabel": {},
"chipActionGoToPlacePage": "Показать в местах",
"@chipActionGoToPlacePage": {},
"settingsModificationWarningDialogMessage": "Другие настройки будут изменены.",
"@settingsModificationWarningDialogMessage": {},
"placePageTitle": "Локации",
"@placePageTitle": {},
"settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.",
"@settingsDisablingBinWarningDialogMessage": {},
"lengthUnitPixel": "пикс.",
"@lengthUnitPixel": {}
} }

View file

@ -1420,5 +1420,17 @@
"lengthUnitPixel": "px", "lengthUnitPixel": "px",
"@lengthUnitPixel": {}, "@lengthUnitPixel": {},
"lengthUnitPercent": "%", "lengthUnitPercent": "%",
"@lengthUnitPercent": {} "@lengthUnitPercent": {},
"settingsVideoEnablePip": "Картинка в картинці",
"@settingsVideoEnablePip": {},
"vaultLockTypePattern": "Шаблон",
"@vaultLockTypePattern": {},
"patternDialogEnter": "Введіть шаблон",
"@patternDialogEnter": {},
"patternDialogConfirm": "Підтвердіть шаблон",
"@patternDialogConfirm": {},
"settingsVideoBackgroundMode": "Фоновий режим",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
"@settingsVideoBackgroundModeDialogTitle": {}
} }

View file

@ -1,5 +1,7 @@
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:floating/floating.dart';
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@ -9,7 +11,7 @@ class Device {
late final String _userAgent; late final String _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint; late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
String get userAgent => _userAgent; String get userAgent => _userAgent;
@ -41,6 +43,8 @@ class Device {
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode; bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
bool get supportPictureInPicture => _supportPictureInPicture;
Device._private(); Device._private();
Future<void> init() async { Future<void> init() async {
@ -53,6 +57,15 @@ class Device {
final auth = LocalAuthentication(); final auth = LocalAuthentication();
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported(); _canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
final floating = Floating();
try {
_supportPictureInPicture = await floating.isPipAvailable;
} on PlatformException catch (_) {
// as of floating v2.0.0, plugin assumes activity and fails when bound via service
_supportPictureInPicture = false;
}
floating.dispose();
final capabilities = await deviceService.getCapabilities(); final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPinShortcut = capabilities['canPinShortcut'] ?? false;

View file

@ -91,6 +91,7 @@ class SettingsDefaults {
// video // video
static const enableVideoHardwareAcceleration = true; static const enableVideoHardwareAcceleration = true;
static const videoAutoPlayMode = VideoAutoPlayMode.disabled; static const videoAutoPlayMode = VideoAutoPlayMode.disabled;
static const videoBackgroundMode = VideoBackgroundMode.disabled;
static const videoLoopMode = VideoLoopMode.shortOnly; static const videoLoopMode = VideoLoopMode.shortOnly;
static const videoShowRawTimedText = false; static const videoShowRawTimedText = false;
static const videoControls = VideoControls.play; static const videoControls = VideoControls.play;

View file

@ -28,12 +28,14 @@ enum ThumbnailOverlayTagIcon { tagged, untagged, none }
enum UnitSystem { metric, imperial } enum UnitSystem { metric, imperial }
enum VideoAutoPlayMode { disabled, playMuted, playWithSound }
enum VideoBackgroundMode { disabled, pip }
enum VideoControls { play, playSeek, playOutside, none } enum VideoControls { play, playSeek, playOutside, none }
enum VideoLoopMode { never, shortOnly, always } enum VideoLoopMode { never, shortOnly, always }
enum VideoAutoPlayMode { disabled, playMuted, playWithSound }
enum ViewerTransition { slide, parallax, fade, zoomIn, none } enum ViewerTransition { slide, parallax, fade, zoomIn, none }
enum WidgetDisplayedItem { random, mostRecent } enum WidgetDisplayedItem { random, mostRecent }

View file

@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
import 'enums.dart'; import 'enums.dart';
extension ExtraSlideshowVideoPlayback on VideoAutoPlayMode { extension ExtraVideoAutoPlayMode on VideoAutoPlayMode {
String getName(BuildContext context) { String getName(BuildContext context) {
switch (this) { switch (this) {
case VideoAutoPlayMode.disabled: case VideoAutoPlayMode.disabled:

View file

@ -0,0 +1,15 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraVideoBackgroundMode on VideoBackgroundMode {
String getName(BuildContext context) {
switch (this) {
case VideoBackgroundMode.disabled:
return context.l10n.settingsDisabled;
case VideoBackgroundMode.pip:
return context.l10n.settingsVideoEnablePip;
}
}
}

View file

@ -1,5 +1,5 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'enums.dart'; import 'enums.dart';

View file

@ -133,6 +133,7 @@ class Settings extends ChangeNotifier {
// video // video
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec'; static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
static const videoBackgroundModeKey = 'video_background_mode';
static const videoAutoPlayModeKey = 'video_auto_play_mode'; static const videoAutoPlayModeKey = 'video_auto_play_mode';
static const videoLoopModeKey = 'video_loop'; static const videoLoopModeKey = 'video_loop';
static const videoControlsKey = 'video_controls'; static const videoControlsKey = 'video_controls';
@ -284,6 +285,7 @@ class Settings extends ChangeNotifier {
viewerGestureSideTapNext = false; viewerGestureSideTapNext = false;
viewerUseCutout = true; viewerUseCutout = true;
viewerMaxBrightness = false; viewerMaxBrightness = false;
videoBackgroundMode = VideoBackgroundMode.disabled;
videoControls = VideoControls.none; videoControls = VideoControls.none;
videoGestureDoubleTapTogglePlay = false; videoGestureDoubleTapTogglePlay = false;
videoGestureSideDoubleTapSeek = false; videoGestureSideDoubleTapSeek = false;
@ -298,6 +300,9 @@ class Settings extends ChangeNotifier {
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) { if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
_set(viewerUseCutoutKey, null); _set(viewerUseCutoutKey, null);
} }
if (videoBackgroundMode == VideoBackgroundMode.pip && !device.supportPictureInPicture) {
_set(videoBackgroundModeKey, null);
}
} }
// app // app
@ -659,6 +664,10 @@ class Settings extends ChangeNotifier {
set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString()); set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString());
VideoBackgroundMode get videoBackgroundMode => getEnumOrDefault(videoBackgroundModeKey, SettingsDefaults.videoBackgroundMode, VideoBackgroundMode.values);
set videoBackgroundMode(VideoBackgroundMode newValue) => _set(videoBackgroundModeKey, newValue.toString());
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values);
set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString()); set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString());
@ -1117,6 +1126,7 @@ class Settings extends ChangeNotifier {
case tagSortFactorKey: case tagSortFactorKey:
case imageBackgroundKey: case imageBackgroundKey:
case videoAutoPlayModeKey: case videoAutoPlayModeKey:
case videoBackgroundModeKey:
case videoLoopModeKey: case videoLoopModeKey:
case videoControlsKey: case videoControlsKey:
case subtitleTextAlignmentKey: case subtitleTextAlignmentKey:

View file

@ -201,7 +201,8 @@ class MediaStoreSource extends CollectionSource {
// so we manually notify change for potential home screen filters // so we manually notify change for potential home screen filters
notifyAlbumsChanged(); notifyAlbumsChanged();
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedEntries.length} removed'); debugPrint('$runtimeType refresh ${stopwatch.elapsed} done');
unawaited(reportService.log('Source refresh complete in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedEntries.length} removed'));
}, },
onError: (error) => debugPrint('$runtimeType stream error=$error'), onError: (error) => debugPrint('$runtimeType stream error=$error'),
); );

View file

@ -1,13 +1,15 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum VaultLockType { system, pin, password } enum VaultLockType { system, pattern, pin, password }
extension ExtraVaultLockType on VaultLockType { extension ExtraVaultLockType on VaultLockType {
String getText(BuildContext context) { String getText(BuildContext context) {
switch (this) { switch (this) {
case VaultLockType.system: case VaultLockType.system:
return context.l10n.settingsSystemDefault; return context.l10n.settingsSystemDefault;
case VaultLockType.pattern:
return context.l10n.vaultLockTypePattern;
case VaultLockType.pin: case VaultLockType.pin:
return context.l10n.vaultLockTypePin; return context.l10n.vaultLockTypePin;
case VaultLockType.password: case VaultLockType.password:

View file

@ -7,6 +7,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/password_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/password_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/pattern_dialog.dart';
import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -160,6 +161,16 @@ class Vaults extends ChangeNotifier {
} }
} }
break; break;
case VaultLockType.pattern:
final pattern = await showDialog<String>(
context: context,
builder: (context) => const PatternDialog(needConfirmation: false),
routeSettings: const RouteSettings(name: PatternDialog.routeName),
);
if (pattern != null) {
confirmed = pattern == await securityService.readValue(details.passKey);
}
break;
case VaultLockType.pin: case VaultLockType.pin:
final pin = await showDialog<String>( final pin = await showDialog<String>(
context: context, context: context,
@ -211,6 +222,16 @@ class Vaults extends ChangeNotifier {
} }
} }
break; break;
case VaultLockType.pattern:
final pattern = await showDialog<String>(
context: context,
builder: (context) => const PatternDialog(needConfirmation: true),
routeSettings: const RouteSettings(name: PatternDialog.routeName),
);
if (pattern != null) {
return await securityService.writeValue(details.passKey, pattern);
}
break;
case VaultLockType.pin: case VaultLockType.pin:
final pin = await showDialog<String>( final pin = await showDialog<String>(
context: context, context: context,

View file

@ -118,6 +118,11 @@ class PlatformMediaEditService implements MediaEditService {
required String destinationAlbum, required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
}) { }) {
// TODO TLAD remove log when OOMs are inspected
entries.where((v) => (v.sizeBytes ?? 0) > 20000000).forEach((entry) {
reportService.log('convert large entry=$entry size=${entry.sizeBytes}');
});
try { try {
return _opStream return _opStream
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{

View file

@ -11,7 +11,11 @@ import 'package:get_it/get_it.dart';
abstract class MediaSessionService { abstract class MediaSessionService {
Stream<MediaCommandEvent> get mediaCommands; Stream<MediaCommandEvent> get mediaCommands;
Future<void> update(AvesVideoController controller); Future<void> update({
required AvesVideoController controller,
required bool canSkipToNext,
required bool canSkipToPrevious,
});
Future<void> release(); Future<void> release();
} }
@ -38,7 +42,11 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>(); Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>();
@override @override
Future<void> update(AvesVideoController controller) async { Future<void> update({
required AvesVideoController controller,
required bool canSkipToNext,
required bool canSkipToPrevious,
}) async {
final entry = controller.entry; final entry = controller.entry;
try { try {
await _platformObject.invokeMethod('update', <String, dynamic>{ await _platformObject.invokeMethod('update', <String, dynamic>{
@ -48,6 +56,8 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
'state': _toPlatformState(controller.status), 'state': _toPlatformState(controller.status),
'positionMillis': controller.currentPosition, 'positionMillis': controller.currentPosition,
'playbackSpeed': controller.speed, 'playbackSpeed': controller.speed,
'canSkipToNext': canSkipToNext,
'canSkipToPrevious': canSkipToPrevious,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -88,6 +98,12 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
case 'pause': case 'pause':
event = const MediaCommandEvent(MediaCommand.pause); event = const MediaCommandEvent(MediaCommand.pause);
break; break;
case 'skip_to_next':
event = const MediaCommandEvent(MediaCommand.skipToNext);
break;
case 'skip_to_previous':
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
break;
case 'stop': case 'stop':
event = const MediaCommandEvent(MediaCommand.stop); event = const MediaCommandEvent(MediaCommand.stop);
break; break;
@ -104,7 +120,7 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
} }
} }
enum MediaCommand { play, pause, stop, seek } enum MediaCommand { play, pause, skipToNext, skipToPrevious, stop, seek }
@immutable @immutable
class MediaCommandEvent extends Equatable { class MediaCommandEvent extends Equatable {

View file

@ -77,6 +77,11 @@ class PlatformMetadataEditService implements MetadataEditService {
Map<MetadataType, dynamic> metadata, { Map<MetadataType, dynamic> metadata, {
bool autoCorrectTrailerOffset = true, bool autoCorrectTrailerOffset = true,
}) async { }) async {
// TODO TLAD remove log when OOMs are inspected
if ((entry.sizeBytes ?? 0) > 20000000) {
await reportService.log('edit metadata of large entry=$entry size=${entry.sizeBytes}');
}
try { try {
final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{ final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{
'entry': entry.toPlatformEntryMap(), 'entry': entry.toPlatformEntryMap(),

View file

@ -1,7 +1,4 @@
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:flutter/foundation.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class Durations { class Durations {
// Flutter animations (with margin) // Flutter animations (with margin)
@ -72,26 +69,6 @@ class Durations {
static const mapIdleDebounceDelay = Duration(milliseconds: 100); static const mapIdleDebounceDelay = Duration(milliseconds: 100);
} }
class DurationsProvider extends StatelessWidget {
final Widget child;
const DurationsProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return ProxyProvider<Settings, DurationsData>(
update: (context, settings, __) {
final enabled = settings.accessibilityAnimations.animate;
return enabled ? DurationsData() : DurationsData.noAnimation();
},
child: child,
);
}
}
@immutable @immutable
class DurationsData { class DurationsData {
// common animations // common animations

View file

@ -125,6 +125,7 @@ class AIcons {
static const IconData setCover = MdiIcons.imageEditOutline; static const IconData setCover = MdiIcons.imageEditOutline;
static const IconData share = Icons.share_outlined; static const IconData share = Icons.share_outlined;
static const IconData show = Icons.visibility_outlined; static const IconData show = Icons.visibility_outlined;
static const IconData showFullscreen = MdiIcons.arrowExpand;
static const IconData slideshow = Icons.slideshow_outlined; static const IconData slideshow = Icons.slideshow_outlined;
static const IconData speed = Icons.speed_outlined; static const IconData speed = Icons.speed_outlined;
static const IconData stats = Icons.donut_small_outlined; static const IconData stats = Icons.donut_small_outlined;

View file

@ -75,6 +75,11 @@ class Dependencies {
license: mit, license: mit,
sourceUrl: 'https://github.com/deckerst/fijkplayer', sourceUrl: 'https://github.com/deckerst/fijkplayer',
), ),
Dependency(
name: 'Floating',
license: mit,
sourceUrl: 'https://github.com/wrbl606/floating',
),
Dependency( Dependency(
name: 'Flutter Display Mode', name: 'Flutter Display Mode',
license: mit, license: mit,
@ -260,6 +265,11 @@ class Dependencies {
license: apache2, license: apache2,
sourceUrl: 'https://github.com/zesage/panorama', sourceUrl: 'https://github.com/zesage/panorama',
), ),
Dependency(
name: 'Pattern Lock',
license: apache2,
sourceUrl: 'https://github.com/qwert2603/pattern_lock',
),
Dependency( Dependency(
name: 'Percent Indicator', name: 'Percent Indicator',
license: bsd2, license: bsd2,

View file

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/flutter_version.dart'; import 'package:aves/flutter_version.dart';
@ -142,7 +141,6 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
} }
Future<String> _getInfo(BuildContext context) async { Future<String> _getInfo(BuildContext context) async {
final accessibility = window.accessibilityFeatures;
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
final androidInfo = await DeviceInfoPlugin().androidInfo; final androidInfo = await DeviceInfoPlugin().androidInfo;
final flavor = context.read<AppFlavor>().toString().split('.')[1]; final flavor = context.read<AppFlavor>().toString().split('.')[1];
@ -161,7 +159,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}', 'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}',
'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}', 'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}',
'Installer: ${packageInfo.installerStore}', 'Installer: ${packageInfo.installerStore}',
'Accessibility: accessibleNavigation=${accessibility.accessibleNavigation}, disableAnimations=${accessibility.disableAnimations}', 'Error reporting: ${settings.isErrorReportingAllowed}',
].join('\n'); ].join('\n');
} }

View file

@ -1,7 +1,6 @@
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/basic/markdown_container.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/intents.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -17,7 +16,6 @@ class PolicyPage extends StatefulWidget {
class _PolicyPageState extends State<PolicyPage> { class _PolicyPageState extends State<PolicyPage> {
late Future<String> _termsLoader; late Future<String> _termsLoader;
final ScrollController _scrollController = ScrollController();
static const termsPath = 'assets/terms.md'; static const termsPath = 'assets/terms.md';
static const termsDirection = TextDirection.ltr; static const termsDirection = TextDirection.ltr;
@ -39,11 +37,8 @@ class _PolicyPageState extends State<PolicyPage> {
child: FocusableActionDetector( child: FocusableActionDetector(
autofocus: true, autofocus: true,
shortcuts: const { shortcuts: const {
SingleActivator(LogicalKeyboardKey.arrowUp): VerticalScrollIntent.up(), SingleActivator(LogicalKeyboardKey.arrowUp): ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
SingleActivator(LogicalKeyboardKey.arrowDown): VerticalScrollIntent.down(), SingleActivator(LogicalKeyboardKey.arrowDown): ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
},
actions: {
VerticalScrollIntent: VerticalScrollIntentAction(scrollController: _scrollController),
}, },
child: Center( child: Center(
child: FutureBuilder<String>( child: FutureBuilder<String>(
@ -54,7 +49,7 @@ class _PolicyPageState extends State<PolicyPage> {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: MarkdownContainer( child: MarkdownContainer(
scrollController: _scrollController, scrollController: PrimaryScrollController.of(context),
data: terms, data: terms,
textDirection: termsDirection, textDirection: termsDirection,
), ),

View file

@ -49,6 +49,7 @@ class AboutTranslators extends StatelessWidget {
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'), Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'), Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
Contributor('kaajjo', 'claymanoff@gmail.com'), Contributor('kaajjo', 'claymanoff@gmail.com'),
Contributor('Eduardo Malaspina', 'vaio0@swismail.com'),
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian

View file

@ -33,6 +33,7 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/durations_provider.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
@ -190,18 +191,16 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// place the settings provider above `MaterialApp` // place the settings provider above `MaterialApp`
// so it can be used during navigation transitions // so it can be used during navigation transitions
return Provider<AppFlavor>.value( return MultiProvider(
value: widget.flavor, providers: [
child: ChangeNotifierProvider<Settings>.value( Provider<AppFlavor>.value(value: widget.flavor),
value: settings, ChangeNotifierProvider<Settings>.value(value: settings),
child: ListenableProvider<ValueNotifier<AppMode>>.value( ListenableProvider<ValueNotifier<AppMode>>.value(value: _appModeNotifier),
value: _appModeNotifier, Provider<CollectionSource>.value(value: _mediaStoreSource),
child: Provider<CollectionSource>.value( Provider<TvRailController>.value(value: _tvRailController),
value: _mediaStoreSource, DurationsProvider(),
child: Provider<TvRailController>.value( HighlightInfoProvider(),
value: _tvRailController, ],
child: DurationsProvider(
child: HighlightInfoProvider(
child: OverlaySupport( child: OverlaySupport(
child: FutureBuilder<void>( child: FutureBuilder<void>(
future: _appSetup, future: _appSetup,
@ -245,9 +244,21 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized); final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return Shortcuts( return Shortcuts(
shortcuts: { shortcuts: {
// handle Android TV remote `select` button // handle Android TV remote `select` button (KEYCODE_DPAD_CENTER)
// the following keys are already handled by default:
// KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
}, },
child: MediaQuery.fromWindow(
child: Builder(
builder: (context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
// disable accessible navigation, as it impacts snack bar action timer
// for all users of apps registered as accessibility services,
// even though they are not for accessibility purposes (like TalkBack is)
accessibleNavigation: false,
),
child: MaterialApp( child: MaterialApp(
navigatorKey: AvesApp.navigatorKey, navigatorKey: AvesApp.navigatorKey,
home: home, home: home,
@ -266,6 +277,11 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
supportedLocales: AvesApp.supportedLocales, supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(), scrollBehavior: StretchMaterialScrollBehavior(),
useInheritedMediaQuery: true,
),
);
},
),
), ),
); );
}, },
@ -275,12 +291,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
}, },
), ),
), ),
),
),
),
),
),
),
); );
} }
@ -354,7 +364,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint('$runtimeType lifecycle ${state.name}');
reportService.log('Lifecycle ${state.name}'); reportService.log('Lifecycle ${state.name}');
switch (state) { switch (state) {
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
@ -547,6 +556,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
'locales': WidgetsBinding.instance.window.locales.join(', '), 'locales': WidgetsBinding.instance.window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
}); });
await reportService.log('Launch');
setState(() => _navigatorObservers = [ setState(() => _navigatorObservers = [
AvesApp.pageRouteObserver, AvesApp.pageRouteObserver,
ReportingRouteTracker(), ReportingRouteTracker(),
@ -557,7 +567,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
debugPrint('$runtimeType onNewIntent with intentData=$intentData'); debugPrint('$runtimeType onNewIntent with intentData=$intentData');
// do not reset when relaunching the app // do not reset when relaunching the app
if (_appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; if (_appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) {
reportService.log('Relaunch');
return;
}
reportService.log('New intent data=$intentData'); reportService.log('New intent data=$intentData');
AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(

View file

@ -23,7 +23,9 @@ import 'package:aves/widgets/common/action_controls/togglers/favourite.dart';
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart'; import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/container.dart';
import 'package:aves/widgets/common/basic/popup/expansion_panel.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
@ -405,10 +407,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}, },
), ),
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash) if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
PopupMenuItem<EntrySetAction>( PopupMenuExpansionPanel<EntrySetAction>(
enabled: hasSelection,
padding: EdgeInsets.zero,
child: PopupMenuItemExpansionPanel<EntrySetAction>(
enabled: hasSelection, enabled: hasSelection,
value: 'edit', value: 'edit',
icon: AIcons.edit, icon: AIcons.edit,
@ -418,7 +417,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), ...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
], ],
), ),
),
]; ];
return [ return [
@ -529,7 +527,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
); );
} }
PopupMenuItem<EntrySetAction> _buildRotateAndFlipMenuItems( PopupMenuEntry<EntrySetAction> _buildRotateAndFlipMenuItems(
BuildContext context, { BuildContext context, {
required bool Function(EntrySetAction action) canApply, required bool Function(EntrySetAction action) canApply,
}) { }) {
@ -558,11 +556,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
), ),
); );
return PopupMenuItem( return PopupMenuItemContainer(
child: TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: Row( child: Row(
children: [ children: [
buildDivider(), buildDivider(),
@ -574,7 +568,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
buildDivider(), buildDivider(),
], ],
), ),
),
); );
} }

View file

@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
@ -19,8 +20,9 @@ import 'package:aves/widgets/collection/draggable_thumb_label.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/tile.dart'; import 'package:aves/widgets/collection/grid/tile.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
@ -39,8 +41,11 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -152,7 +157,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
tileAnimationDelay = Duration.zero; tileAnimationDelay = Duration.zero;
} }
return StreamBuilder( return NotificationListener<OpenViewerNotification>(
onNotification: (notification) {
_goToViewer(collection, notification.entry);
return true;
},
child: StreamBuilder(
stream: source.eventBus.on<AspectRatioChangedEvent>(), stream: source.eventBus.on<AspectRatioChangedEvent>(),
builder: (context, snapshot) => SectionedEntryListLayoutProvider( builder: (context, snapshot) => SectionedEntryListLayoutProvider(
collection: collection, collection: collection,
@ -205,6 +215,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
tileAnimationDelay: tileAnimationDelay, tileAnimationDelay: tileAnimationDelay,
child: child!, child: child!,
), ),
),
); );
}, },
child: child, child: child,
@ -227,6 +238,36 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
}, },
); );
} }
void _goToViewer(CollectionLens collection, AvesEntry entry) {
final selection = context.read<Selection<AvesEntry>>();
Navigator.maybeOf(context)?.push(
TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
final viewerCollection = collection.copyWith(
listenToSource: false,
);
Widget child = EntryViewerPage(
collection: viewerCollection,
initialEntry: entry,
);
if (selection.isSelecting) {
child = MultiProvider(
providers: [
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickMediaInternal)),
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
],
child: child,
);
}
return child;
},
),
);
}
} }
class _CollectionSectionedContent extends StatefulWidget { class _CollectionSectionedContent extends StatefulWidget {
@ -460,6 +501,8 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>( return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
selector: (context, layout) => layout.sectionLayouts, selector: (context, layout) => layout.sectionLayouts,
builder: (context, sectionLayouts, child) { builder: (context, sectionLayouts, child) {
final scrollController = widget.scrollController;
final offsetIncrementSnapThreshold = context.select<TileExtentController, double>((v) => (v.extentNotifier.value + v.spacing) / 4);
return DraggableScrollbar( return DraggableScrollbar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight), scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
@ -467,7 +510,23 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
height: avesScrollThumbHeight, height: avesScrollThumbHeight,
backgroundColor: Colors.white, backgroundColor: Colors.white,
), ),
controller: widget.scrollController, controller: scrollController,
dragOffsetSnapper: (scrollOffset, offsetIncrement) {
if (offsetIncrement > offsetIncrementSnapThreshold && scrollOffset < scrollController.position.maxScrollExtent) {
final section = sectionLayouts.firstWhereOrNull((section) => section.hasChildAtOffset(scrollOffset));
if (section != null) {
if (section.maxOffset - section.minOffset < scrollController.position.viewportDimension) {
// snap to section header
return section.minOffset;
} else {
// snap to content row
final index = section.getMinChildIndexForScrollOffset(scrollOffset);
return section.indexToLayoutOffset(index);
}
}
}
return scrollOffset;
},
crumbsBuilder: () => _getCrumbs(sectionLayouts), crumbsBuilder: () => _getCrumbs(sectionLayouts),
padding: EdgeInsets.only( padding: EdgeInsets.only(
// padding to keep scroll thumb between app bar above and nav bar below // padding to keep scroll thumb between app bar above and nav bar below

View file

@ -14,7 +14,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/intent_service.dart'; import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.dart'; import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
class _CollectionPageState extends State<CollectionPage> { class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
late CollectionLens _collection; late CollectionLens _collection;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast(); final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override @override
@ -146,7 +146,7 @@ class _CollectionPageState extends State<CollectionPage> {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate); final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>( return NotificationListener<DraggableScrollbarNotification>(
onNotification: (notification) { onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event); _draggableScrollBarEventStreamController.add(notification.event);
return false; return false;
@ -222,6 +222,7 @@ class _CollectionPageState extends State<CollectionPage> {
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget; final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
await Future.delayed(delayDuration + Durations.highlightScrollInitDelay); await Future.delayed(delayDuration + Durations.highlightScrollInitDelay);
if (!mounted) return;
final animate = context.read<Settings>().accessibilityAnimations.animate; final animate = context.read<Settings>().accessibilityAnimations.animate;
context.read<HighlightInfo>().trackItem(item, animate: animate, highlightItem: item); context.read<HighlightInfo>().trackItem(item, animate: animate, highlightItem: item);
} }

View file

@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/format.dart'; import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
@ -50,46 +51,57 @@ class EntryListDetails extends StatelessWidget {
); );
} }
Widget _buildRow(List<InlineSpan> spans, TextStyle style) {
return Text.rich(
TextSpan(
children: spans,
),
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
);
}
WidgetSpan _buildIconSpan(IconData icon, {EdgeInsetsDirectional padding = EdgeInsetsDirectional.zero}) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 8, bottom: 1) + padding,
child: Icon(icon),
),
);
}
Widget _buildDateRow(BuildContext context, TextStyle style) { Widget _buildDateRow(BuildContext context, TextStyle style) {
final locale = context.l10n.localeName; final locale = context.l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat); final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final date = entry.bestDate; final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown; final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
return Row( final size = entry.sizeBytes;
children: [ final sizeText = size != null ? formatFileSize(locale, size) : Constants.overlayUnknown;
const Icon(AIcons.date),
const SizedBox(width: 8), return _buildRow(
Expanded( [
child: Text( _buildIconSpan(AIcons.date),
dateText, TextSpan(text: dateText),
style: style, _buildIconSpan(AIcons.size, padding: const EdgeInsetsDirectional.only(start: 8)),
strutStyle: Constants.overflowStrutStyle, TextSpan(text: sizeText),
softWrap: false,
overflow: TextOverflow.fade,
),
),
], ],
style,
); );
} }
Widget _buildLocationRow(BuildContext context, TextStyle style) { Widget _buildLocationRow(BuildContext context, TextStyle style) {
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!); final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
return Row( return _buildRow(
children: [ [
const Icon(AIcons.location), _buildIconSpan(AIcons.location),
const SizedBox(width: 8), TextSpan(text: location),
Expanded(
child: Text(
location,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
], ],
style,
); );
} }
} }

View file

@ -6,10 +6,9 @@ import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/intent_service.dart'; import 'package:aves/services/intent_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart'; import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/grid/scaling.dart'; import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -40,7 +39,7 @@ class InteractiveTile extends StatelessWidget {
if (selection.isSelecting) { if (selection.isSelecting) {
selection.toggleSelection(entry); selection.toggleSelection(entry);
} else { } else {
_goToViewer(context); OpenViewerNotification(entry).dispatch(context);
} }
break; break;
case AppMode.pickSingleMediaExternal: case AppMode.pickSingleMediaExternal:
@ -79,24 +78,6 @@ class InteractiveTile extends StatelessWidget {
), ),
); );
} }
void _goToViewer(BuildContext context) {
Navigator.maybeOf(context)?.push(
TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
final viewerCollection = collection.copyWith(
listenToSource: false,
);
assert(viewerCollection.sortedEntries.map((entry) => entry.id).contains(entry.id));
return EntryViewerPage(
collection: viewerCollection,
initialEntry: entry,
);
},
),
);
}
} }
class Tile extends StatelessWidget { class Tile extends StatelessWidget {

View file

@ -56,6 +56,19 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _hasChooser = hasChooser; final _hasChooser = hasChooser;
Widget child = IconButton(
icon: icon,
onPressed: widget.onPressed,
focusNode: widget.focusNode,
tooltip: _hasChooser ? null : tooltip,
);
if (_hasChooser) {
child = Semantics(
tooltip: tooltip,
child: child,
);
}
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onLongPressStart: _hasChooser ? _showChooser : null, onLongPressStart: _hasChooser ? _showChooser : null,
@ -70,12 +83,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
} }
: null, : null,
onLongPressCancel: _clearChooserOverlayEntry, onLongPressCancel: _clearChooserOverlayEntry,
child: IconButton( child: child,
icon: icon,
onPressed: widget.onPressed,
focusNode: widget.focusNode,
tooltip: _hasChooser ? null : tooltip,
),
); );
} }

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/actions/share_actions.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ShareQuickChooser extends StatelessWidget { class ShareQuickChooser extends StatelessWidget {

View file

@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/fx/sweeper.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';

View file

@ -2,7 +2,7 @@ import 'dart:async';
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/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';

View file

@ -1,6 +1,6 @@
import 'package:aves/model/query.dart'; import 'package:aves/model/query.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -30,7 +30,7 @@ import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart'; import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -0,0 +1,38 @@
import 'package:flutter/rendering.dart';
class ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0.0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0.0);
path.lineTo(0.0, 0.0);
path.close();
const arrowWidth = 8.0;
final startPointX = (size.width - arrowWidth) / 2;
var startPointY = size.height / 2 - arrowWidth / 2;
path.moveTo(startPointX, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
path.lineTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
path.lineTo(startPointX, startPointY + 1.0);
path.close();
startPointY = size.height / 2 + arrowWidth / 2;
path.moveTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
path.lineTo(startPointX, startPointY);
path.lineTo(startPointX, startPointY - 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

View file

@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
@immutable
class DraggableScrollbarNotification extends Notification {
final DraggableScrollbarEvent event;
const DraggableScrollbarNotification(this.event);
}
enum DraggableScrollbarEvent { dragStart, dragEnd }

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class ScrollLabel extends StatelessWidget {
final Animation<double> animation;
final Color backgroundColor;
final Widget child;
const ScrollLabel({
super.key,
required this.child,
required this.animation,
required this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: animation,
child: Container(
margin: const EdgeInsetsDirectional.only(end: 12.0),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: child,
),
),
);
}
}

View file

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:flutter/material.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/scroll_label.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/transition.dart';
import 'package:flutter/widgets.dart';
/* /*
adapted from package `draggable_scrollbar` v0.0.4: adapted from package `draggable_scrollbar` v0.0.4:
@ -57,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget {
/// The ScrollController for the BoxScrollView /// The ScrollController for the BoxScrollView
final ScrollController controller; final ScrollController controller;
final double Function(double scrollOffset, double offsetIncrement)? dragOffsetSnapper;
/// The view that will be scrolled with the scroll thumb /// The view that will be scrolled with the scroll thumb
final ScrollView child; final ScrollView child;
@ -66,6 +70,7 @@ class DraggableScrollbar extends StatefulWidget {
required this.scrollThumbSize, required this.scrollThumbSize,
required this.scrollThumbBuilder, required this.scrollThumbBuilder,
required this.controller, required this.controller,
this.dragOffsetSnapper,
this.crumbsBuilder, this.crumbsBuilder,
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
this.scrollbarAnimationDuration = const Duration(milliseconds: 300), this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
@ -109,38 +114,10 @@ class DraggableScrollbar extends StatefulWidget {
} }
} }
class ScrollLabel extends StatelessWidget {
final Animation<double> animation;
final Color backgroundColor;
final Widget child;
const ScrollLabel({
super.key,
required this.child,
required this.animation,
required this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: animation,
child: Container(
margin: const EdgeInsetsDirectional.only(end: 12.0),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: child,
),
),
);
}
}
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin { class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
bool _isDragInProcess = false; bool _isDragInProcess = false;
double _boundlessThumbOffset = 0, _offsetIncrement = 0;
late Offset _longPressLastGlobalPosition; late Offset _longPressLastGlobalPosition;
late AnimationController _thumbAnimationController; late AnimationController _thumbAnimationController;
@ -239,7 +216,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
), ),
); );
}), }),
RepaintBoundary( // exclude semantics, otherwise this layer will block access to content layers below when using TalkBack
ExcludeSemantics(
child: RepaintBoundary(
child: GestureDetector( child: GestureDetector(
onLongPressStart: (details) { onLongPressStart: (details) {
_longPressLastGlobalPosition = details.globalPosition; _longPressLastGlobalPosition = details.globalPosition;
@ -275,6 +254,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
), ),
), ),
), ),
),
], ],
), ),
); );
@ -304,7 +284,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
} }
void _onVerticalDragStart() { void _onVerticalDragStart() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragStart).dispatch(context); const DraggableScrollbarNotification(DraggableScrollbarEvent.dragStart).dispatch(context);
_boundlessThumbOffset = _thumbOffsetNotifier.value;
_offsetIncrement = 1 / thumbMaxScrollExtent * controller.position.maxScrollExtent;
_labelAnimationController.forward(); _labelAnimationController.forward();
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
_showThumb(); _showThumb();
@ -316,17 +298,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
_showThumb(); _showThumb();
if (_isDragInProcess) { if (_isDragInProcess) {
// thumb offset // thumb offset
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); _boundlessThumbOffset += deltaY;
_thumbOffsetNotifier.value = _boundlessThumbOffset.clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
// scroll offset // scroll offset
final min = controller.position.minScrollExtent; final min = controller.position.minScrollExtent;
final max = controller.position.maxScrollExtent; final max = controller.position.maxScrollExtent;
controller.jumpTo((_thumbOffsetNotifier.value / thumbMaxScrollExtent * max).clamp(min, max)); final scrollOffset = _thumbOffsetNotifier.value / thumbMaxScrollExtent * max;
controller.jumpTo((widget.dragOffsetSnapper?.call(scrollOffset, _offsetIncrement) ?? scrollOffset).clamp(min, max));
} }
} }
void _onVerticalDragEnd() { void _onVerticalDragEnd() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context); const DraggableScrollbarNotification(DraggableScrollbarEvent.dragEnd).dispatch(context);
_scheduleFadeout(); _scheduleFadeout();
setState(() => _isDragInProcess = false); setState(() => _isDragInProcess = false);
} }
@ -373,79 +357,3 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
} }
} }
} }
///This cut 2 lines in arrow shape
class ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0.0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0.0);
path.lineTo(0.0, 0.0);
path.close();
const arrowWidth = 8.0;
final startPointX = (size.width - arrowWidth) / 2;
var startPointY = size.height / 2 - arrowWidth / 2;
path.moveTo(startPointX, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
path.lineTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
path.lineTo(startPointX, startPointY + 1.0);
path.close();
startPointY = size.height / 2 + arrowWidth / 2;
path.moveTo(startPointX + arrowWidth, startPointY);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
path.lineTo(startPointX, startPointY);
path.lineTo(startPointX, startPointY - 1.0);
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
class SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
child: SlideTransition(
position: Tween(
begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
),
);
}
}
@immutable
class DraggableScrollBarNotification extends Notification {
final DraggableScrollBarEvent event;
const DraggableScrollBarNotification(this.event);
}
enum DraggableScrollBarEvent { dragStart, dragEnd }

View file

@ -0,0 +1,31 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
class SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const SlideFadeTransition({
super.key,
required this.animation,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
child: SlideTransition(
position: Tween(
begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
),
);
}
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class PopupMenuItemContainer<T> extends PopupMenuEntry<T> {
final Widget child;
const PopupMenuItemContainer({
super.key,
this.height = kMinInteractiveDimension,
required this.child,
});
@override
final double height;
@override
bool represents(void value) => false;
@override
State<PopupMenuItemContainer> createState() => _TransitionPopupMenuItemState();
}
class _TransitionPopupMenuItemState extends State<PopupMenuItemContainer> {
@override
Widget build(BuildContext context) {
return TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: widget.child,
),
);
}
}

View file

@ -1,62 +1,9 @@
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MenuRow extends StatelessWidget { class PopupMenuExpansionPanel<T> extends PopupMenuEntry<T> {
final String text;
final Widget? icon;
const MenuRow({
super.key,
required this.text,
this.icon,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: IconTheme.merge(
data: IconThemeData(
color: ListTileTheme.of(context).iconColor,
),
child: icon!,
),
),
Flexible(
child: Text(text),
),
],
);
}
}
// scale icons according to text scale
class MenuIconTheme extends StatelessWidget {
final Widget child;
const MenuIconTheme({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
final iconTheme = IconTheme.of(context);
return IconTheme(
data: iconTheme.copyWith(
size: iconTheme.size! * MediaQuery.textScaleFactorOf(context),
),
child: child,
);
}
}
class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
final bool enabled; final bool enabled;
final String value; final String value;
final ValueNotifier<String?> expandedNotifier; final ValueNotifier<String?> expandedNotifier;
@ -64,9 +11,10 @@ class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
final String title; final String title;
final List<PopupMenuEntry<T>> items; final List<PopupMenuEntry<T>> items;
PopupMenuItemExpansionPanel({ PopupMenuExpansionPanel({
super.key, super.key,
this.enabled = true, this.enabled = true,
this.height = kMinInteractiveDimension,
required this.value, required this.value,
ValueNotifier<String?>? expandedNotifier, ValueNotifier<String?>? expandedNotifier,
required this.icon, required this.icon,
@ -75,10 +23,16 @@ class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
}) : expandedNotifier = expandedNotifier ?? ValueNotifier(null); }) : expandedNotifier = expandedNotifier ?? ValueNotifier(null);
@override @override
State<PopupMenuItemExpansionPanel<T>> createState() => _PopupMenuItemExpansionPanelState<T>(); final double height;
@override
bool represents(void value) => false;
@override
State<PopupMenuExpansionPanel<T>> createState() => _PopupMenuExpansionPanelState<T>();
} }
class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionPanel<T>> { class _PopupMenuExpansionPanelState<T> extends State<PopupMenuExpansionPanel<T>> {
// ref `_kMenuHorizontalPadding` used in `PopupMenuItem` // ref `_kMenuHorizontalPadding` used in `PopupMenuItem`
static const double _horizontalPadding = 16; static const double _horizontalPadding = 16;
@ -103,8 +57,13 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
elevation: 0, elevation: 0,
children: [ children: [
ExpansionPanel( ExpansionPanel(
headerBuilder: (context, isExpanded) => DefaultTextStyle( headerBuilder: (context, isExpanded) {
return DefaultTextStyle(
style: style, style: style,
child: IconTheme.merge(
data: IconThemeData(
color: widget.enabled ? null : Theme.of(context).disabledColor,
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding), padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
child: MenuRow( child: MenuRow(
@ -113,6 +72,8 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
), ),
), ),
), ),
);
},
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
class MenuRow extends StatelessWidget {
final String text;
final Widget? icon;
const MenuRow({
super.key,
required this.text,
this.icon,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: IconTheme.merge(
data: IconThemeData(
color: ListTileTheme.of(context).iconColor,
),
child: icon!,
),
),
Flexible(
child: Text(text),
),
],
);
}
}
// scale icons according to text scale
class MenuIconTheme extends StatelessWidget {
final Widget child;
const MenuIconTheme({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
final iconTheme = IconTheme.of(context);
return IconTheme(
data: iconTheme.copyWith(
size: iconTheme.size! * MediaQuery.textScaleFactorOf(context),
),
child: child,
);
}
}

View file

@ -73,8 +73,8 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
children: _diffs.map((diff) { children: _diffs.map((diff) {
final oldText = diff.item1; final oldText = diff.item1;
final newText = diff.item2; final newText = diff.item2;
final oldWidth = diff.item3; final oldSize = diff.item3;
final newWidth = diff.item4; final newSize = diff.item4;
final text = (_animation.value == 0 ? oldText : newText) ?? ''; final text = (_animation.value == 0 ? oldText : newText) ?? '';
return WidgetSpan( return WidgetSpan(
child: AnimatedSize( child: AnimatedSize(
@ -91,9 +91,10 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
children: [ children: [
...previousChildren.map( ...previousChildren.map(
(child) => ConstrainedBox( (child) => ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints.tight(Size(
maxWidth: min(oldWidth, newWidth), min(oldSize.width, newSize.width),
), min(oldSize.height, newSize.height),
)),
child: child, child: child,
), ),
), ),
@ -116,14 +117,16 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
); );
} }
double textWidth(String text) { Size textSize(String text) {
final para = RenderParagraph( final para = RenderParagraph(
TextSpan(text: text, style: widget.textStyle), TextSpan(text: text, style: widget.textStyle),
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
strutStyle: widget.strutStyle, strutStyle: widget.strutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true); )..layout(const BoxConstraints(), parentUsesSize: true);
return para.getMaxIntrinsicWidth(double.infinity); final width = para.getMaxIntrinsicWidth(double.infinity);
final height = para.getMaxIntrinsicHeight(double.infinity);
return Size(width, height);
} }
// use an adaptation of Google's `Diff Match and Patch` // use an adaptation of Google's `Diff Match and Patch`
@ -140,15 +143,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
..clear() ..clear()
..addAll(d.map((diff) { ..addAll(d.map((diff) {
final text = diff.text; final text = diff.text;
final size = textSize(text);
switch (diff.operation) { switch (diff.operation) {
case Operation.delete: case Operation.delete:
return Tuple4(text, null, textWidth(text), .0); return Tuple4(text, null, size, Size.zero);
case Operation.insert: case Operation.insert:
return Tuple4(null, text, .0, textWidth(text)); return Tuple4(null, text, Size.zero, size);
case Operation.equal: case Operation.equal:
default: default:
final width = textWidth(text); return Tuple4(text, text, size, size);
return Tuple4(text, text, width, width);
} }
}).fold<List<_TextDiff>>([], (prev, v) { }).fold<List<_TextDiff>>([], (prev, v) {
if (prev.isNotEmpty) { if (prev.isNotEmpty) {
@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
return [...prev, v]; return [...prev, v];
})); }));
} }
// void _computeDiff(String oldText, String newText) {
// final oldCharacters = oldText.characters.toList();
// final newCharacters = newText.characters.toList();
// final diffResult = calculateListDiff<String>(oldCharacters, newCharacters, detectMoves: false);
// final updates = diffResult.getUpdatesWithData().toList();
// List<TextDiff> diffs = [];
// DataDiffUpdate<String>? pendingUpdate;
// int lastPos = oldCharacters.length;
// void addKeep(int pos) {
// if (pos < lastPos) {
// final text = oldCharacters.sublist(pos, lastPos).join();
// final width = textWidth(text);
// diffs.insert(0, Tuple4(text, text, width, width));
// lastPos = pos;
// }
// }
//
// void commit(DataDiffUpdate<String>? update) {
// update?.when(
// insert: (pos, data) {
// addKeep(pos);
// diffs.insert(0, Tuple4(null, data, 0, textWidth(data)));
// lastPos = pos;
// },
// remove: (pos, data) {
// addKeep(pos + data.length);
// diffs.insert(0, Tuple4(data, null, textWidth(data), 0));
// lastPos = pos;
// },
// change: (pos, oldData, newData) {
// addKeep(pos + oldData.length);
// diffs.insert(0, Tuple4(oldData, newData, textWidth(oldData), textWidth(newData)));
// lastPos = pos;
// },
// move: (from, to, data) {
// assert(false, '`move` update: from=$from, to=$from, data=$data');
// },
// );
// }
//
// for (var update in updates) {
// update.when(
// insert: (pos, data) {
// if (pendingUpdate == null) {
// pendingUpdate = update;
// return;
// }
// if (pendingUpdate is DataInsert) {
// final pendingInsert = pendingUpdate as DataInsert;
// if (pendingInsert.position == pos) {
// // merge insertions
// pendingUpdate = DataInsert(position: pos, data: data + pendingInsert.data);
// return;
// }
// } else if (pendingUpdate is DataRemove) {
// final pendingRemove = pendingUpdate as DataRemove;
// if (pendingRemove.position == pos) {
// // convert to change
// pendingUpdate = DataChange(position: pos, oldData: pendingRemove.data, newData: data);
// return;
// }
// } else if (pendingUpdate is DataChange) {
// final pendingChange = pendingUpdate as DataChange;
// if (pendingChange.position == pos) {
// // merge changes
// pendingUpdate = DataChange(position: pos, oldData: pendingChange.oldData, newData: data + pendingChange.newData);
// return;
// }
// }
// commit(pendingUpdate);
// pendingUpdate = update;
// },
// remove: (pos, data) {
// if (pendingUpdate == null) {
// pendingUpdate = update;
// return;
// }
// if (pendingUpdate is DataRemove) {
// final pendingRemove = pendingUpdate as DataRemove;
// if (pendingRemove.position == pos + data.length) {
// // merge removals
// pendingUpdate = DataRemove(position: pos, data: data + pendingRemove.data);
// return;
// }
// }
// commit(pendingUpdate);
// pendingUpdate = update;
// },
// change: (pos, oldData, newData) {
// assert(false, '`change` update: from=$pos, oldData=$oldData, newData=$newData');
// },
// move: (from, to, data) {
// assert(false, '`move` update: from=$from, to=$from, data=$data');
// },
// );
// }
// commit(pendingUpdate);
// addKeep(0);
// _diffs
// ..clear()
// ..addAll(diffs);
// }
} }
typedef _TextDiff = Tuple4<String?, String?, double, double>; typedef _TextDiff = Tuple4<String?, String?, Size, Size>;

View file

@ -1,37 +1,22 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class VerticalScrollIntent extends Intent { class ScrollControllerAction extends CallbackAction<ScrollIntent> {
const VerticalScrollIntent({ ScrollControllerAction({
required this.type,
});
const VerticalScrollIntent.up() : type = VerticalScrollDirection.up;
const VerticalScrollIntent.down() : type = VerticalScrollDirection.down;
final VerticalScrollDirection type;
}
enum VerticalScrollDirection {
up,
down,
}
class VerticalScrollIntentAction extends CallbackAction<VerticalScrollIntent> {
VerticalScrollIntentAction({
required ScrollController scrollController, required ScrollController scrollController,
}) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController)); }) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController));
static void _onScrollIntent( static void _onScrollIntent(
VerticalScrollIntent intent, ScrollIntent intent,
ScrollController scrollController, ScrollController scrollController,
) { ) {
late int factor; late int factor;
switch (intent.type) { switch (intent.direction) {
case VerticalScrollDirection.up: case AxisDirection.up:
case AxisDirection.left:
factor = -1; factor = -1;
break; break;
case VerticalScrollDirection.down: case AxisDirection.down:
case AxisDirection.right:
factor = 1; factor = 1;
break; break;
} }

View file

@ -22,28 +22,13 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting); final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting);
final child = isSelecting return AnimatedSwitcher(
? Selector<Selection<T>, bool>(
selector: (context, selection) => selection.isSelected([item]),
builder: (context, isSelected, child) {
var child = isSelecting
? OverlayIcon(
key: ValueKey(isSelected),
icon: isSelected ? AIcons.selected : AIcons.unselected,
margin: EdgeInsets.zero,
)
: const SizedBox();
child = AnimatedSwitcher(
duration: duration, duration: duration,
switchInCurve: Curves.easeOutBack, child: isSelecting
switchOutCurve: Curves.easeOutBack, ? Selector<Selection<T>, bool>(
transitionBuilder: (child, animation) => ScaleTransition( selector: (context, selection) => selection.isSelected({item}),
scale: animation, builder: (context, isSelected, child) {
child: child, return AnimatedContainer(
),
child: child,
);
child = AnimatedContainer(
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
padding: padding, padding: padding,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -51,15 +36,24 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
duration: duration, duration: duration,
child: AnimatedSwitcher(
duration: duration,
switchInCurve: Curves.easeOutBack,
switchOutCurve: Curves.easeOutBack,
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child, child: child,
),
child: OverlayIcon(
key: ValueKey(isSelected),
icon: isSelected ? AIcons.selected : AIcons.unselected,
margin: EdgeInsets.zero,
),
),
); );
return child;
}, },
) )
: const SizedBox(); : const SizedBox(),
return AnimatedSwitcher(
duration: duration,
child: child,
); );
} }
} }

View file

@ -206,30 +206,34 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
void _toggleSelectionToIndex(int toIndex) { void _toggleSelectionToIndex(int toIndex) {
if (toIndex == -1) return; if (toIndex == -1) return;
Iterable<T> getRange(int start, int end) => items.getRange(start, end);
final selection = context.read<Selection<T>>(); final selection = context.read<Selection<T>>();
void addRange(int start, int end) => selection.addToSelection(getRange(start, end));
void removeRange(int start, int end) => selection.removeFromSelection(getRange(start, end));
if (_selecting) { if (_selecting) {
if (toIndex <= _fromIndex) { if (toIndex <= _fromIndex) {
if (toIndex < _lastToIndex) { if (toIndex < _lastToIndex) {
selection.addToSelection(items.getRange(toIndex, min(_fromIndex, _lastToIndex))); addRange(toIndex, min(_fromIndex, _lastToIndex));
if (_fromIndex < _lastToIndex) { if (_fromIndex < _lastToIndex) {
selection.removeFromSelection(items.getRange(_fromIndex + 1, _lastToIndex + 1)); removeRange(_fromIndex + 1, _lastToIndex + 1);
} }
} else if (_lastToIndex < toIndex) { } else if (_lastToIndex < toIndex) {
selection.removeFromSelection(items.getRange(_lastToIndex, toIndex)); removeRange(_lastToIndex, toIndex);
} }
} else if (_fromIndex < toIndex) { } else if (_fromIndex < toIndex) {
if (_lastToIndex < toIndex) { if (_lastToIndex < toIndex) {
selection.addToSelection(items.getRange(max(_fromIndex, _lastToIndex), toIndex + 1)); addRange(max(_fromIndex, _lastToIndex), toIndex + 1);
if (_lastToIndex < _fromIndex) { if (_lastToIndex < _fromIndex) {
selection.removeFromSelection(items.getRange(_lastToIndex, _fromIndex)); removeRange(_lastToIndex, _fromIndex);
} }
} else if (toIndex < _lastToIndex) { } else if (toIndex < _lastToIndex) {
selection.removeFromSelection(items.getRange(toIndex + 1, _lastToIndex + 1)); removeRange(toIndex + 1, _lastToIndex + 1);
} }
} }
_lastToIndex = toIndex; _lastToIndex = toIndex;
} else { } else {
selection.removeFromSelection(items.getRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1)); removeRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1);
} }
} }
} }

View file

@ -30,10 +30,12 @@ class GridTheme extends StatelessWidget {
final fontSize = (iconSize * .7).floorToDouble(); final fontSize = (iconSize * .7).floorToDouble();
iconSize *= mq.textScaleFactor; iconSize *= mq.textScaleFactor;
final highlightBorderWidth = extent * .1; final highlightBorderWidth = extent * .1;
final interactiveDimension = min(iconSize * 2, kMinInteractiveDimension);
return GridThemeData( return GridThemeData(
iconSize: iconSize, iconSize: iconSize,
fontSize: fontSize, fontSize: fontSize,
highlightBorderWidth: highlightBorderWidth, highlightBorderWidth: highlightBorderWidth,
interactiveDimension: interactiveDimension,
showFavourite: settings.showThumbnailFavourite, showFavourite: settings.showThumbnailFavourite,
locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none, locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none,
tagIcon: settings.thumbnailTagIcon, tagIcon: settings.thumbnailTagIcon,
@ -52,7 +54,7 @@ class GridTheme extends StatelessWidget {
typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesEntry entry); typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesEntry entry);
class GridThemeData { class GridThemeData {
final double iconSize, fontSize, highlightBorderWidth; final double iconSize, fontSize, highlightBorderWidth, interactiveDimension;
final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration; final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
final bool showLocated, showUnlocated, showTagged, showUntagged; final bool showLocated, showUnlocated, showTagged, showUntagged;
late final GridThemeIconBuilder iconBuilder; late final GridThemeIconBuilder iconBuilder;
@ -61,6 +63,7 @@ class GridThemeData {
required this.iconSize, required this.iconSize,
required this.fontSize, required this.fontSize,
required this.highlightBorderWidth, required this.highlightBorderWidth,
required this.interactiveDimension,
required this.showFavourite, required this.showFavourite,
required ThumbnailOverlayLocationIcon locationIcon, required ThumbnailOverlayLocationIcon locationIcon,
required ThumbnailOverlayTagIcon tagIcon, required ThumbnailOverlayTagIcon tagIcon,

View file

@ -14,7 +14,7 @@ import 'package:aves/theme/colors.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/collection/filter_bar.dart'; import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/arrow_clipper.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const double avesScrollThumbHeight = 48; const double avesScrollThumbHeight = 48;

View file

@ -5,7 +5,7 @@ import 'package:aves/theme/themes.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
); );
} }
break; break;
case MapNavigationButton.none:
break;
} }
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter); final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);

View file

@ -256,7 +256,10 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
} }
Future<void> _resetRotation() async { Future<void> _resetRotation() async {
final rotationTween = Tween<double>(begin: _leafletMapController.rotation, end: 0); final rotation = _leafletMapController.rotation;
// prevent multiple turns
final begin = (rotation.abs() % 360) * rotation.sign;
final rotationTween = Tween<double>(begin: begin, end: 0);
await _animateCamera((animation) => _leafletMapController.rotate(rotationTween.evaluate(animation))); await _animateCamera((animation) => _leafletMapController.rotate(rotationTween.evaluate(animation)));
} }

View file

@ -0,0 +1,16 @@
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:provider/provider.dart';
class DurationsProvider extends ProxyProvider<Settings, DurationsData> {
DurationsProvider({
super.key,
super.child,
}) : super(
update: (context, settings, __) {
final enabled = settings.accessibilityAnimations.animate;
return enabled ? DurationsData() : DurationsData.noAnimation();
},
);
}

View file

@ -1,20 +1,11 @@
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class HighlightInfoProvider extends StatelessWidget { class HighlightInfoProvider extends ChangeNotifierProvider<HighlightInfo> {
final Widget child; HighlightInfoProvider({
const HighlightInfoProvider({
super.key, super.key,
required this.child, super.child,
}); }) : super(
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<HighlightInfo>(
create: (context) => HighlightInfo(), create: (context) => HighlightInfo(),
child: child,
); );
}
} }

View file

@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/common/grid/overlay.dart'; import 'package:aves/widgets/common/grid/overlay.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/common/thumbnail/overlay.dart'; import 'package:aves/widgets/common/thumbnail/overlay.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -44,11 +45,15 @@ class DecoratedThumbnail extends StatelessWidget {
children: [ children: [
child, child,
ThumbnailEntryOverlay(entry: entry), ThumbnailEntryOverlay(entry: entry),
if (selectable) if (selectable) ...[
GridItemSelectionOverlay<AvesEntry>( GridItemSelectionOverlay<AvesEntry>(
item: entry, item: entry,
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
), ),
ThumbnailZoomOverlay(
onZoom: () => OpenViewerNotification(entry).dispatch(context),
),
],
if (highlightable) ThumbnailHighlightOverlay(entry: entry), if (highlightable) ThumbnailHighlightOverlay(entry: entry),
], ],
); );

View file

@ -0,0 +1,9 @@
import 'package:aves/model/entry.dart';
import 'package:flutter/widgets.dart';
@immutable
class OpenViewerNotification extends Notification {
final AvesEntry entry;
const OpenViewerNotification(this.entry);
}

View file

@ -2,6 +2,9 @@ import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/fx/sweeper.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -70,3 +73,43 @@ class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
); );
} }
} }
class ThumbnailZoomOverlay extends StatelessWidget {
final VoidCallback? onZoom;
const ThumbnailZoomOverlay({
super.key,
this.onZoom,
});
static const alignment = AlignmentDirectional.bottomEnd;
static const duration = Durations.thumbnailOverlayAnimation;
@override
Widget build(BuildContext context) {
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
final interactiveDimension = context.select<GridThemeData, double>((t) => t.interactiveDimension);
return AnimatedSwitcher(
duration: duration,
child: isSelecting
? Align(
alignment: alignment,
child: GestureDetector(
onTap: onZoom,
child: Container(
alignment: alignment,
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 2),
width: interactiveDimension,
height: interactiveDimension,
child: Icon(
AIcons.showFullscreen,
size: context.select<GridThemeData, double>((t) => t.iconSize),
color: Colors.white70,
),
),
),
)
: const SizedBox(),
);
}
}

View file

@ -87,14 +87,14 @@ class TileExtentController {
int _effectiveColumnCountForExtent(double extent) { int _effectiveColumnCountForExtent(double extent) {
if (extent > 0) { if (extent > 0) {
final columnCount = _columnCountForExtent(extent); final columnCount = _columnCountForExtent(extent);
final countMin = _effectiveColumnCountMin();
final countMax = _effectiveColumnCountMax(); final countMax = _effectiveColumnCountMax();
final countMin = min(_effectiveColumnCountMin(), countMax);
return columnCount.round().clamp(countMin, countMax); return columnCount.round().clamp(countMin, countMax);
} }
return columnCountDefault; return columnCountDefault;
} }
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax()); double get effectiveExtentMin => min(_extentForColumnCount(_effectiveColumnCountMax()), effectiveExtentMax);
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin()); double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());

View file

@ -8,7 +8,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/analysis_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart'; import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';

View file

@ -7,7 +7,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';

View file

@ -10,7 +10,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/basic/color_list_tile.dart'; import 'package:aves/widgets/common/basic/list_tiles/color.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';

View file

@ -39,6 +39,7 @@ class _EditVaultDialogState extends State<EditVaultDialog> {
final List<VaultLockType> _lockTypeOptions = [ final List<VaultLockType> _lockTypeOptions = [
if (device.canAuthenticateUser) VaultLockType.system, if (device.canAuthenticateUser) VaultLockType.system,
if (device.canUseCrypto) ...[ if (device.canUseCrypto) ...[
VaultLockType.pattern,
VaultLockType.pin, VaultLockType.pin,
VaultLockType.password, VaultLockType.password,
], ],

View file

@ -41,7 +41,16 @@ class _PasswordDialogState extends State<PasswordDialog> {
controller: _controller, controller: _controller,
focusNode: _focusNode, focusNode: _focusNode,
obscureText: true, obscureText: true,
onSubmitted: (password) { onSubmitted: _submit,
autofillHints: const [AutofillHints.password],
),
),
],
),
);
}
void _submit(String password) {
if (widget.needConfirmation) { if (widget.needConfirmation) {
if (_confirming) { if (_confirming) {
final match = _firstPassword == password; final match = _firstPassword == password;
@ -65,12 +74,5 @@ class _PasswordDialogState extends State<PasswordDialog> {
} else { } else {
Navigator.maybeOf(context)?.pop<String>(password); Navigator.maybeOf(context)?.pop<String>(password);
} }
},
autofillHints: const [AutofillHints.password],
),
),
],
),
);
} }
} }

View file

@ -0,0 +1,75 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart';
import 'package:pattern_lock/pattern_lock.dart';
import 'package:provider/provider.dart';
class PatternDialog extends StatefulWidget {
static const routeName = '/dialog/pattern';
final bool needConfirmation;
const PatternDialog({
super.key,
required this.needConfirmation,
});
@override
State<PatternDialog> createState() => _PatternDialogState();
}
class _PatternDialogState extends State<PatternDialog> {
bool _confirming = false;
String? _firstPattern;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return AvesDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(_confirming ? context.l10n.patternDialogConfirm : context.l10n.patternDialogEnter),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: SizedBox.square(
dimension: context.select<MediaQueryData, double>((mq) => mq.size.shortestSide / 2),
child: PatternLock(
relativePadding: .4,
selectedColor: colorScheme.secondary,
notSelectedColor: colorScheme.onBackground,
pointRadius: 8,
fillPoints: true,
onInputComplete: (input) => _submit(input.join()),
),
),
),
],
),
);
}
void _submit(String pattern) {
if (widget.needConfirmation) {
if (_confirming) {
final match = _firstPattern == pattern;
Navigator.maybeOf(context)?.pop<String>(match ? pattern : null);
if (!match) {
showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.genericFailureFeedback),
actions: const [OkButton()],
),
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
);
}
} else {
_firstPattern = pattern;
setState(() => _confirming = true);
}
} else {
Navigator.maybeOf(context)?.pop<String>(pattern);
}
}
}

View file

@ -38,7 +38,27 @@ class _PinDialogState extends State<PinDialog> {
controller: _controller, controller: _controller,
obscureText: true, obscureText: true,
onChanged: (v) {}, onChanged: (v) {},
onCompleted: (pin) { onCompleted: _submit,
animationType: AnimationType.scale,
keyboardType: TextInputType.number,
autoFocus: true,
autoDismissKeyboard: !widget.needConfirmation || _confirming,
pinTheme: PinTheme(
activeColor: colorScheme.onBackground,
inactiveColor: colorScheme.onBackground,
selectedColor: colorScheme.secondary,
selectedFillColor: colorScheme.secondary,
borderRadius: BorderRadius.circular(8),
shape: PinCodeFieldShape.box,
),
),
),
],
),
);
}
void _submit(String pin) {
if (widget.needConfirmation) { if (widget.needConfirmation) {
if (_confirming) { if (_confirming) {
final match = _firstPin == pin; final match = _firstPin == pin;
@ -61,23 +81,5 @@ class _PinDialogState extends State<PinDialog> {
} else { } else {
Navigator.maybeOf(context)?.pop<String>(pin); Navigator.maybeOf(context)?.pop<String>(pin);
} }
},
animationType: AnimationType.scale,
keyboardType: TextInputType.number,
autoFocus: true,
autoDismissKeyboard: !widget.needConfirmation || _confirming,
pinTheme: PinTheme(
activeColor: colorScheme.onBackground,
inactiveColor: colorScheme.onBackground,
selectedColor: colorScheme.secondary,
selectedFillColor: colorScheme.secondary,
borderRadius: BorderRadius.circular(8),
shape: PinCodeFieldShape.box,
),
),
),
],
),
);
} }
} }

View file

@ -13,7 +13,7 @@ import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.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/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';

View file

@ -2,8 +2,8 @@ import 'package:aves/image_providers/app_icon_image_provider.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/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/query_bar.dart';
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -11,7 +11,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart'; import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';

View file

@ -12,7 +12,8 @@ import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.dart'; import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
@ -61,7 +62,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final QueryTest<T> applyQuery; final QueryTest<T> applyQuery;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final HeroType heroType; final HeroType heroType;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast(); final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
FilterGridPage({ FilterGridPage({
super.key, super.key,
@ -145,7 +146,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate); final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>( return NotificationListener<DraggableScrollbarNotification>(
onNotification: (notification) { onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event); _draggableScrollBarEventStreamController.add(notification.event);
return false; return false;

View file

@ -229,9 +229,12 @@ class _HomePageState extends State<HomePage> {
} }
} }
break; break;
case AppMode.setWallpaper:
// for video playback storage
await metadataDb.init();
break;
case AppMode.pickMediaInternal: case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal: case AppMode.pickFilterInternal:
case AppMode.setWallpaper:
case AppMode.slideshow: case AppMode.slideshow:
break; break;
} }

View file

@ -18,7 +18,7 @@ import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -32,7 +32,7 @@ import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/map/map_info_row.dart'; import 'package:aves/widgets/map/map_info_row.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

Some files were not shown because too many files have changed in this diff Show more