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="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
### Added

View file

@ -67,6 +67,16 @@ This change eventually prevents building the app with Flutter v3.3.3.
<intent>
<action android:name="android.intent.action.MAIN" />
</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,
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:hardwareAccelerated="true"
android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:theme="@style/NormalTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@ -283,7 +294,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
<meta-data
android:name="flutterEmbedding"
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
android:name="io.flutter.embedding.android.EnableImpeller"
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.WindowHandler
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.enableSoftwareRendering
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterFragmentActivity() {
private lateinit var intentDataMap: MutableMap<String, Any?>
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onCreate(savedInstanceState: Bundle?) {
if (FlutterUtils.isSoftwareRenderingRequired()) {
@ -42,12 +45,19 @@ class WallpaperActivity : FlutterFragmentActivity() {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart
// - need Context
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper
@ -79,6 +89,11 @@ class WallpaperActivity : FlutterFragmentActivity() {
}
}
override fun onDestroy() {
mediaSessionHandler.dispose()
super.onDestroy()
}
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getIntentData" -> {

View file

@ -285,7 +285,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
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()
// 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)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
.putExtra(Intent.EXTRA_STREAM, uri)
} else {
var mimeType = "*/*"
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 positionMillis = call.argument<Number>("positionMillis")?.toLong()
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(
"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
}
@ -90,6 +92,12 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
} else {
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()
.setState(

View file

@ -46,6 +46,16 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
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() {
super.onStop()
success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
@ -70,6 +80,8 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
const val COMMAND_PLAY = "play"
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_SEEK = "seek"
}

View file

@ -44,8 +44,14 @@ object StorageUtils {
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 {
val dirs = context.getExternalFilesDirs(null).filterNotNull()
val dirs = listOf(
*context.getExternalFilesDirs(null).filterNotNull().toTypedArray(),
context.filesDir,
)
return dirs.any { path.startsWith(it.path) }
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<!-- API28+, draws next to the notch in fullscreen -->

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<!-- 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>. Пользователи могут легко переходить от альбомов к фотографиям, тэгам, картам и т.д.
<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,
}.contains(this);
bool get canEditEntry => {
AppMode.main,
AppMode.view,
}.contains(this);
bool get canSelectMedia => {
AppMode.main,
AppMode.pickMultipleMediaExternal,

View file

@ -1366,5 +1366,65 @@
"settingsViewerShowDescription": "Zobrazit popis",
"@settingsViewerShowDescription": {},
"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",
"unitSystemImperial": "Imperial",
"vaultLockTypePin": "Pin",
"vaultLockTypePattern": "Pattern",
"vaultLockTypePin": "PIN",
"vaultLockTypePassword": "Password",
"settingsVideoEnablePip": "Picture-in-picture",
"videoControlsPlay": "Play",
"videoControlsPlaySeek": "Play & seek backward/forward",
"videoControlsPlayOutside": "Open with other player",
@ -384,8 +387,11 @@
"vaultDialogLockModeWhenScreenOff": "Lock when screen turns off",
"vaultDialogLockTypeLabel": "Lock type",
"pinDialogEnter": "Enter pin",
"pinDialogConfirm": "Confirm pin",
"patternDialogEnter": "Enter pattern",
"patternDialogConfirm": "Confirm pattern",
"pinDialogEnter": "Enter PIN",
"pinDialogConfirm": "Confirm PIN",
"passwordDialogEnter": "Enter password",
"passwordDialogConfirm": "Confirm password",
@ -792,6 +798,8 @@
"settingsVideoAutoPlay": "Auto play",
"settingsVideoLoopModeTile": "Loop mode",
"settingsVideoLoopModeDialogTitle": "Loop Mode",
"settingsVideoBackgroundMode": "Background mode",
"settingsVideoBackgroundModeDialogTitle": "Background Mode",
"settingsSubtitleThemeTile": "Subtitles",
"settingsSubtitleThemePageTitle": "Subtitles",

View file

@ -1262,5 +1262,17 @@
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"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": {},
"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": {},
"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": {},
"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": {},
"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": {},
"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": {},
"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": {},
"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": {},
"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": {},
"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: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:package_info_plus/package_info_plus.dart';
@ -9,7 +11,7 @@ class Device {
late final String _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
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;
@ -41,6 +43,8 @@ class Device {
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
bool get supportPictureInPicture => _supportPictureInPicture;
Device._private();
Future<void> init() async {
@ -53,6 +57,15 @@ class Device {
final auth = LocalAuthentication();
_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();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false;

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSlideshowVideoPlayback on VideoAutoPlayMode {
extension ExtraVideoAutoPlayMode on VideoAutoPlayMode {
String getName(BuildContext context) {
switch (this) {
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/viewer/controller.dart';
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';

View file

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

View file

@ -201,7 +201,8 @@ class MediaStoreSource extends CollectionSource {
// so we manually notify change for potential home screen filters
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'),
);

View file

@ -1,13 +1,15 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
enum VaultLockType { system, pin, password }
enum VaultLockType { system, pattern, pin, password }
extension ExtraVaultLockType on VaultLockType {
String getText(BuildContext context) {
switch (this) {
case VaultLockType.system:
return context.l10n.settingsSystemDefault;
case VaultLockType.pattern:
return context.l10n.vaultLockTypePattern;
case VaultLockType.pin:
return context.l10n.vaultLockTypePin;
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/dialogs/aves_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:collection/collection.dart';
import 'package:flutter/material.dart';
@ -160,6 +161,16 @@ class Vaults extends ChangeNotifier {
}
}
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:
final pin = await showDialog<String>(
context: context,
@ -211,6 +222,16 @@ class Vaults extends ChangeNotifier {
}
}
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:
final pin = await showDialog<String>(
context: context,

View file

@ -118,6 +118,11 @@ class PlatformMediaEditService implements MediaEditService {
required String destinationAlbum,
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 {
return _opStream
.receiveBroadcastStream(<String, dynamic>{

View file

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

View file

@ -77,6 +77,11 @@ class PlatformMetadataEditService implements MetadataEditService {
Map<MetadataType, dynamic> metadata, {
bool autoCorrectTrailerOffset = true,
}) 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 {
final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{
'entry': entry.toPlatformEntryMap(),

View file

@ -1,7 +1,4 @@
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:flutter/foundation.dart';
class Durations {
// Flutter animations (with margin)
@ -72,26 +69,6 @@ class Durations {
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
class DurationsData {
// common animations

View file

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

View file

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

View file

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

View file

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

View file

@ -49,6 +49,7 @@ class AboutTranslators extends StatelessWidget {
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
Contributor('kaajjo', 'claymanoff@gmail.com'),
Contributor('Eduardo Malaspina', 'vaio0@swismail.com'),
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
// 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/routes.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/media_query_data_provider.dart';
import 'package:aves/widgets/home_page.dart';
@ -190,95 +191,104 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Widget build(BuildContext context) {
// place the settings provider above `MaterialApp`
// so it can be used during navigation transitions
return Provider<AppFlavor>.value(
value: widget.flavor,
child: ChangeNotifierProvider<Settings>.value(
value: settings,
child: ListenableProvider<ValueNotifier<AppMode>>.value(
value: _appModeNotifier,
child: Provider<CollectionSource>.value(
value: _mediaStoreSource,
child: Provider<TvRailController>.value(
value: _tvRailController,
child: DurationsProvider(
child: HighlightInfoProvider(
child: OverlaySupport(
child: FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
if (initialized) {
AvesApp.showSystemUI();
}
final home = initialized
? _getFirstPage()
: AvesScaffold(
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
);
return Selector<Settings, Tuple3<Locale?, AvesThemeBrightness, bool>>(
selector: (context, s) => Tuple3(
s.locale,
s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final themeBrightness = s.item2;
final enableDynamicColor = s.item3;
return MultiProvider(
providers: [
Provider<AppFlavor>.value(value: widget.flavor),
ChangeNotifierProvider<Settings>.value(value: settings),
ListenableProvider<ValueNotifier<AppMode>>.value(value: _appModeNotifier),
Provider<CollectionSource>.value(value: _mediaStoreSource),
Provider<TvRailController>.value(value: _tvRailController),
DurationsProvider(),
HighlightInfoProvider(),
],
child: OverlaySupport(
child: FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
if (initialized) {
AvesApp.showSystemUI();
}
final home = initialized
? _getFirstPage()
: AvesScaffold(
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
);
return Selector<Settings, Tuple3<Locale?, AvesThemeBrightness, bool>>(
selector: (context, s) => Tuple3(
s.locale,
s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final themeBrightness = s.item2;
final enableDynamicColor = s.item3;
Constants.updateStylesForLocale(settings.appliedLocale);
Constants.updateStylesForLocale(settings.appliedLocale);
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return Shortcuts(
shortcuts: {
// handle Android TV remote `select` button
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => _decorateAppChild(
context: context,
initialized: initialized,
child: child,
),
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
),
);
},
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return Shortcuts(
shortcuts: {
// 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(),
},
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(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) => _decorateAppChild(
context: context,
initialized: initialized,
child: child,
),
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
useInheritedMediaQuery: true,
),
);
},
);
},
),
),
),
),
),
),
),
),
);
},
);
},
);
},
),
),
);
@ -354,7 +364,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint('$runtimeType lifecycle ${state.name}');
reportService.log('Lifecycle ${state.name}');
switch (state) {
case AppLifecycleState.inactive:
@ -547,6 +556,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
'locales': WidgetsBinding.instance.window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
});
await reportService.log('Launch');
setState(() => _navigatorObservers = [
AvesApp.pageRouteObserver,
ReportingRouteTracker(),
@ -557,7 +567,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
debugPrint('$runtimeType onNewIntent with intentData=$intentData');
// 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');
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/app_bar/app_bar_subtitle.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/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
@ -405,19 +407,15 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
},
),
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
PopupMenuItem<EntrySetAction>(
PopupMenuExpansionPanel<EntrySetAction>(
enabled: hasSelection,
padding: EdgeInsets.zero,
child: PopupMenuItemExpansionPanel<EntrySetAction>(
enabled: hasSelection,
value: 'edit',
icon: AIcons.edit,
title: context.l10n.collectionActionEdit,
items: [
_buildRotateAndFlipMenuItems(context, canApply: canApply),
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
],
),
value: 'edit',
icon: AIcons.edit,
title: context.l10n.collectionActionEdit,
items: [
_buildRotateAndFlipMenuItems(context, canApply: canApply),
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
],
),
];
@ -529,7 +527,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
}
PopupMenuItem<EntrySetAction> _buildRotateAndFlipMenuItems(
PopupMenuEntry<EntrySetAction> _buildRotateAndFlipMenuItems(
BuildContext context, {
required bool Function(EntrySetAction action) canApply,
}) {
@ -558,22 +556,17 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
),
);
return PopupMenuItem(
child: TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: Row(
children: [
buildDivider(),
buildItem(EntrySetAction.rotateCCW),
buildDivider(),
buildItem(EntrySetAction.rotateCW),
buildDivider(),
buildItem(EntrySetAction.flip),
buildDivider(),
],
),
return PopupMenuItemContainer(
child: Row(
children: [
buildDivider(),
buildItem(EntrySetAction.rotateCCW),
buildDivider(),
buildItem(EntrySetAction.rotateCW),
buildDivider(),
buildItem(EntrySetAction.flip),
buildDivider(),
],
),
);
}

View file

@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/favourite.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/source/collection_lens.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/section_layout.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/behaviour/routes.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/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/thumbnail/decorated.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/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/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -152,58 +157,64 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
tileAnimationDelay = Duration.zero;
}
return StreamBuilder(
stream: source.eventBus.on<AspectRatioChangedEvent>(),
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
collection: collection,
selectable: selectable,
scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount,
spacing: tileSpacing,
horizontalPadding: horizontalPadding,
tileExtent: thumbnailExtent,
tileBuilder: (entry, tileSize) {
final extent = tileSize.shortestSide;
return AnimatedBuilder(
animation: favourites,
builder: (context, child) {
Widget tile = InteractiveTile(
key: ValueKey(entry.id),
collection: collection,
entry: entry,
thumbnailExtent: extent,
tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier,
);
if (!settings.useTvLayout) return tile;
return NotificationListener<OpenViewerNotification>(
onNotification: (notification) {
_goToViewer(collection, notification.entry);
return true;
},
child: StreamBuilder(
stream: source.eventBus.on<AspectRatioChangedEvent>(),
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
collection: collection,
selectable: selectable,
scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount,
spacing: tileSpacing,
horizontalPadding: horizontalPadding,
tileExtent: thumbnailExtent,
tileBuilder: (entry, tileSize) {
final extent = tileSize.shortestSide;
return AnimatedBuilder(
animation: favourites,
builder: (context, child) {
Widget tile = InteractiveTile(
key: ValueKey(entry.id),
collection: collection,
entry: entry,
thumbnailExtent: extent,
tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier,
);
if (!settings.useTvLayout) return tile;
return Focus(
onFocusChange: (focused) {
if (focused) {
_focusedItemNotifier.value = entry;
} else if (_focusedItemNotifier.value == entry) {
_focusedItemNotifier.value = null;
}
},
child: ValueListenableBuilder<AvesEntry?>(
valueListenable: _focusedItemNotifier,
builder: (context, focusedItem, child) {
return AnimatedScale(
scale: focusedItem == entry ? 1 : .9,
curve: Curves.fastOutSlowIn,
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
child: child!,
);
return Focus(
onFocusChange: (focused) {
if (focused) {
_focusedItemNotifier.value = entry;
} else if (_focusedItemNotifier.value == entry) {
_focusedItemNotifier.value = null;
}
},
child: tile,
),
);
},
);
},
tileAnimationDelay: tileAnimationDelay,
child: child!,
child: ValueListenableBuilder<AvesEntry?>(
valueListenable: _focusedItemNotifier,
builder: (context, focusedItem, child) {
return AnimatedScale(
scale: focusedItem == entry ? 1 : .9,
curve: Curves.fastOutSlowIn,
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
child: child!,
);
},
child: tile,
),
);
},
);
},
tileAnimationDelay: tileAnimationDelay,
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 {
@ -460,6 +501,8 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
selector: (context, layout) => layout.sectionLayouts,
builder: (context, sectionLayouts, child) {
final scrollController = widget.scrollController;
final offsetIncrementSnapThreshold = context.select<TileExtentController, double>((v) => (v.extentNotifier.value + v.spacing) / 4);
return DraggableScrollbar(
backgroundColor: Colors.white,
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
@ -467,7 +510,23 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
height: avesScrollThumbHeight,
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),
padding: EdgeInsets.only(
// 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/theme/durations.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/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = [];
late CollectionLens _collection;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
@ -146,7 +146,7 @@ class _CollectionPageState extends State<CollectionPage> {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>(
return NotificationListener<DraggableScrollbarNotification>(
onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event);
return false;
@ -222,6 +222,7 @@ class _CollectionPageState extends State<CollectionPage> {
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
await Future.delayed(delayDuration + Durations.highlightScrollInitDelay);
if (!mounted) return;
final animate = context.read<Settings>().accessibilityAnimations.animate;
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/icons.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/common/extensions/build_context.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) {
final locale = context.l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
return Row(
children: [
const Icon(AIcons.date),
const SizedBox(width: 8),
Expanded(
child: Text(
dateText,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
final size = entry.sizeBytes;
final sizeText = size != null ? formatFileSize(locale, size) : Constants.overlayUnknown;
return _buildRow(
[
_buildIconSpan(AIcons.date),
TextSpan(text: dateText),
_buildIconSpan(AIcons.size, padding: const EdgeInsetsDirectional.only(start: 8)),
TextSpan(text: sizeText),
],
style,
);
}
Widget _buildLocationRow(BuildContext context, TextStyle style) {
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
return Row(
children: [
const Icon(AIcons.location),
const SizedBox(width: 8),
Expanded(
child: Text(
location,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
return _buildRow(
[
_buildIconSpan(AIcons.location),
TextSpan(text: location),
],
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/widgets/collection/grid/list_details.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/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:provider/provider.dart';
@ -40,7 +39,7 @@ class InteractiveTile extends StatelessWidget {
if (selection.isSelecting) {
selection.toggleSelection(entry);
} else {
_goToViewer(context);
OpenViewerNotification(entry).dispatch(context);
}
break;
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 {

View file

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

View file

@ -2,7 +2,7 @@ import 'dart:async';
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/basic/menu.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:flutter/material.dart';
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/theme/colors.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/fx/sweeper.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async';
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/identity/buttons/captioned_button.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/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/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/viewer/video/controller.dart';

View file

@ -1,6 +1,6 @@
import 'package:aves/model/query.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/identity/buttons/captioned_button.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/convert_entry_dialog.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:flutter/material.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 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.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:
@ -57,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget {
/// The ScrollController for the BoxScrollView
final ScrollController controller;
final double Function(double scrollOffset, double offsetIncrement)? dragOffsetSnapper;
/// The view that will be scrolled with the scroll thumb
final ScrollView child;
@ -66,6 +70,7 @@ class DraggableScrollbar extends StatefulWidget {
required this.scrollThumbSize,
required this.scrollThumbBuilder,
required this.controller,
this.dragOffsetSnapper,
this.crumbsBuilder,
this.padding = EdgeInsets.zero,
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 {
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
bool _isDragInProcess = false;
double _boundlessThumbOffset = 0, _offsetIncrement = 0;
late Offset _longPressLastGlobalPosition;
late AnimationController _thumbAnimationController;
@ -239,37 +216,40 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
),
);
}),
RepaintBoundary(
child: GestureDetector(
onLongPressStart: (details) {
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragStart();
},
onLongPressMoveUpdate: (details) {
final dy = (details.globalPosition - _longPressLastGlobalPosition).dy;
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragUpdate(dy);
},
onLongPressEnd: (_) => _onVerticalDragEnd(),
onVerticalDragStart: (_) => _onVerticalDragStart(),
onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy),
onVerticalDragEnd: (_) => _onVerticalDragEnd(),
child: ValueListenableBuilder<double>(
valueListenable: _thumbOffsetNotifier,
builder: (context, thumbOffset, child) => Container(
alignment: AlignmentDirectional.topEnd,
padding: EdgeInsets.only(top: thumbOffset) + widget.padding,
child: widget.scrollThumbBuilder(
widget.backgroundColor,
_thumbAnimation,
_labelAnimation,
widget.scrollThumbSize.height,
labelText: _isDragInProcess
? ValueListenableBuilder<double>(
valueListenable: _viewOffsetNotifier,
builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset),
)
: null,
// exclude semantics, otherwise this layer will block access to content layers below when using TalkBack
ExcludeSemantics(
child: RepaintBoundary(
child: GestureDetector(
onLongPressStart: (details) {
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragStart();
},
onLongPressMoveUpdate: (details) {
final dy = (details.globalPosition - _longPressLastGlobalPosition).dy;
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragUpdate(dy);
},
onLongPressEnd: (_) => _onVerticalDragEnd(),
onVerticalDragStart: (_) => _onVerticalDragStart(),
onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy),
onVerticalDragEnd: (_) => _onVerticalDragEnd(),
child: ValueListenableBuilder<double>(
valueListenable: _thumbOffsetNotifier,
builder: (context, thumbOffset, child) => Container(
alignment: AlignmentDirectional.topEnd,
padding: EdgeInsets.only(top: thumbOffset) + widget.padding,
child: widget.scrollThumbBuilder(
widget.backgroundColor,
_thumbAnimation,
_labelAnimation,
widget.scrollThumbSize.height,
labelText: _isDragInProcess
? ValueListenableBuilder<double>(
valueListenable: _viewOffsetNotifier,
builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset),
)
: null,
),
),
),
),
@ -304,7 +284,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
}
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();
_fadeoutTimer?.cancel();
_showThumb();
@ -316,17 +298,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
_showThumb();
if (_isDragInProcess) {
// thumb offset
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
_boundlessThumbOffset += deltaY;
_thumbOffsetNotifier.value = _boundlessThumbOffset.clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
// scroll offset
final min = controller.position.minScrollExtent;
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() {
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context);
const DraggableScrollbarNotification(DraggableScrollbarEvent.dragEnd).dispatch(context);
_scheduleFadeout();
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/widgets/common/basic/popup/menu_row.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.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,
);
}
}
class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
class PopupMenuExpansionPanel<T> extends PopupMenuEntry<T> {
final bool enabled;
final String value;
final ValueNotifier<String?> expandedNotifier;
@ -64,9 +11,10 @@ class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
final String title;
final List<PopupMenuEntry<T>> items;
PopupMenuItemExpansionPanel({
PopupMenuExpansionPanel({
super.key,
this.enabled = true,
this.height = kMinInteractiveDimension,
required this.value,
ValueNotifier<String?>? expandedNotifier,
required this.icon,
@ -75,10 +23,16 @@ class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
}) : expandedNotifier = expandedNotifier ?? ValueNotifier(null);
@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`
static const double _horizontalPadding = 16;
@ -103,16 +57,23 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
elevation: 0,
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) => DefaultTextStyle(
style: style,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
child: MenuRow(
text: widget.title,
icon: Icon(widget.icon),
headerBuilder: (context, isExpanded) {
return DefaultTextStyle(
style: style,
child: IconTheme.merge(
data: IconThemeData(
color: widget.enabled ? null : Theme.of(context).disabledColor,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
child: MenuRow(
text: widget.title,
icon: Icon(widget.icon),
),
),
),
),
),
);
},
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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) {
final oldText = diff.item1;
final newText = diff.item2;
final oldWidth = diff.item3;
final newWidth = diff.item4;
final oldSize = diff.item3;
final newSize = diff.item4;
final text = (_animation.value == 0 ? oldText : newText) ?? '';
return WidgetSpan(
child: AnimatedSize(
@ -91,9 +91,10 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
children: [
...previousChildren.map(
(child) => ConstrainedBox(
constraints: BoxConstraints(
maxWidth: min(oldWidth, newWidth),
),
constraints: BoxConstraints.tight(Size(
min(oldSize.width, newSize.width),
min(oldSize.height, newSize.height),
)),
child: child,
),
),
@ -116,14 +117,16 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
);
}
double textWidth(String text) {
Size textSize(String text) {
final para = RenderParagraph(
TextSpan(text: text, style: widget.textStyle),
textDirection: Directionality.of(context),
textScaleFactor: MediaQuery.textScaleFactorOf(context),
strutStyle: widget.strutStyle,
)..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`
@ -140,15 +143,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
..clear()
..addAll(d.map((diff) {
final text = diff.text;
final size = textSize(text);
switch (diff.operation) {
case Operation.delete:
return Tuple4(text, null, textWidth(text), .0);
return Tuple4(text, null, size, Size.zero);
case Operation.insert:
return Tuple4(null, text, .0, textWidth(text));
return Tuple4(null, text, Size.zero, size);
case Operation.equal:
default:
final width = textWidth(text);
return Tuple4(text, text, width, width);
return Tuple4(text, text, size, size);
}
}).fold<List<_TextDiff>>([], (prev, v) {
if (prev.isNotEmpty) {
@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
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';
class VerticalScrollIntent extends Intent {
const VerticalScrollIntent({
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({
class ScrollControllerAction extends CallbackAction<ScrollIntent> {
ScrollControllerAction({
required ScrollController scrollController,
}) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController));
static void _onScrollIntent(
VerticalScrollIntent intent,
ScrollIntent intent,
ScrollController scrollController,
) {
late int factor;
switch (intent.type) {
case VerticalScrollDirection.up:
switch (intent.direction) {
case AxisDirection.up:
case AxisDirection.left:
factor = -1;
break;
case VerticalScrollDirection.down:
case AxisDirection.down:
case AxisDirection.right:
factor = 1;
break;
}

View file

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

View file

@ -206,30 +206,34 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
void _toggleSelectionToIndex(int toIndex) {
if (toIndex == -1) return;
Iterable<T> getRange(int start, int end) => items.getRange(start, end);
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 (toIndex <= _fromIndex) {
if (toIndex < _lastToIndex) {
selection.addToSelection(items.getRange(toIndex, min(_fromIndex, _lastToIndex)));
addRange(toIndex, min(_fromIndex, _lastToIndex));
if (_fromIndex < _lastToIndex) {
selection.removeFromSelection(items.getRange(_fromIndex + 1, _lastToIndex + 1));
removeRange(_fromIndex + 1, _lastToIndex + 1);
}
} else if (_lastToIndex < toIndex) {
selection.removeFromSelection(items.getRange(_lastToIndex, toIndex));
removeRange(_lastToIndex, toIndex);
}
} else if (_fromIndex < toIndex) {
if (_lastToIndex < toIndex) {
selection.addToSelection(items.getRange(max(_fromIndex, _lastToIndex), toIndex + 1));
addRange(max(_fromIndex, _lastToIndex), toIndex + 1);
if (_lastToIndex < _fromIndex) {
selection.removeFromSelection(items.getRange(_lastToIndex, _fromIndex));
removeRange(_lastToIndex, _fromIndex);
}
} else if (toIndex < _lastToIndex) {
selection.removeFromSelection(items.getRange(toIndex + 1, _lastToIndex + 1));
removeRange(toIndex + 1, _lastToIndex + 1);
}
}
_lastToIndex = toIndex;
} 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();
iconSize *= mq.textScaleFactor;
final highlightBorderWidth = extent * .1;
final interactiveDimension = min(iconSize * 2, kMinInteractiveDimension);
return GridThemeData(
iconSize: iconSize,
fontSize: fontSize,
highlightBorderWidth: highlightBorderWidth,
interactiveDimension: interactiveDimension,
showFavourite: settings.showThumbnailFavourite,
locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none,
tagIcon: settings.thumbnailTagIcon,
@ -52,7 +54,7 @@ class GridTheme extends StatelessWidget {
typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesEntry entry);
class GridThemeData {
final double iconSize, fontSize, highlightBorderWidth;
final double iconSize, fontSize, highlightBorderWidth, interactiveDimension;
final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
final bool showLocated, showUnlocated, showTagged, showUntagged;
late final GridThemeIconBuilder iconBuilder;
@ -61,6 +63,7 @@ class GridThemeData {
required this.iconSize,
required this.fontSize,
required this.highlightBorderWidth,
required this.interactiveDimension,
required this.showFavourite,
required ThumbnailOverlayLocationIcon locationIcon,
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/icons.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/providers/media_query_data_provider.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';
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/widgets/common/fx/blurred.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:flutter/material.dart';
import 'package:provider/provider.dart';

View file

@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
);
}
break;
case MapNavigationButton.none:
break;
}
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 {
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)));
}

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:flutter/widgets.dart';
import 'package:provider/provider.dart';
class HighlightInfoProvider extends StatelessWidget {
final Widget child;
const HighlightInfoProvider({
class HighlightInfoProvider extends ChangeNotifierProvider<HighlightInfo> {
HighlightInfoProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<HighlightInfo>(
create: (context) => HighlightInfo(),
child: child,
);
}
super.child,
}) : super(
create: (context) => HighlightInfo(),
);
}

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/grid/overlay.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:flutter/material.dart';
@ -44,11 +45,15 @@ class DecoratedThumbnail extends StatelessWidget {
children: [
child,
ThumbnailEntryOverlay(entry: entry),
if (selectable)
if (selectable) ...[
GridItemSelectionOverlay<AvesEntry>(
item: entry,
padding: const EdgeInsets.all(2),
),
ThumbnailZoomOverlay(
onZoom: () => OpenViewerNotification(entry).dispatch(context),
),
],
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/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/grid/theme.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) {
if (extent > 0) {
final columnCount = _columnCountForExtent(extent);
final countMin = _effectiveColumnCountMin();
final countMax = _effectiveColumnCountMax();
final countMin = min(_effectiveColumnCountMin(), countMax);
return columnCount.round().clamp(countMin, countMax);
}
return columnCountDefault;
}
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
double get effectiveExtentMin => min(_extentForColumnCount(_effectiveColumnCountMax()), effectiveExtentMax);
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/theme/durations.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/behaviour/pop/scope.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/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/scheduler.dart';

View file

@ -7,7 +7,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.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/extensions/build_context.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/theme/icons.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/fx/borders.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';

View file

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

View file

@ -41,31 +41,7 @@ class _PasswordDialogState extends State<PasswordDialog> {
controller: _controller,
focusNode: _focusNode,
obscureText: true,
onSubmitted: (password) {
if (widget.needConfirmation) {
if (_confirming) {
final match = _firstPassword == password;
Navigator.maybeOf(context)?.pop<String>(match ? password : null);
if (!match) {
showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.genericFailureFeedback),
actions: const [OkButton()],
),
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
);
}
} else {
_firstPassword = password;
_controller.clear();
setState(() => _confirming = true);
WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus());
}
} else {
Navigator.maybeOf(context)?.pop<String>(password);
}
},
onSubmitted: _submit,
autofillHints: const [AutofillHints.password],
),
),
@ -73,4 +49,30 @@ class _PasswordDialogState extends State<PasswordDialog> {
),
);
}
void _submit(String password) {
if (widget.needConfirmation) {
if (_confirming) {
final match = _firstPassword == password;
Navigator.maybeOf(context)?.pop<String>(match ? password : null);
if (!match) {
showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.genericFailureFeedback),
actions: const [OkButton()],
),
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
);
}
} else {
_firstPassword = password;
_controller.clear();
setState(() => _confirming = true);
WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus());
}
} else {
Navigator.maybeOf(context)?.pop<String>(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,30 +38,7 @@ class _PinDialogState extends State<PinDialog> {
controller: _controller,
obscureText: true,
onChanged: (v) {},
onCompleted: (pin) {
if (widget.needConfirmation) {
if (_confirming) {
final match = _firstPin == pin;
Navigator.maybeOf(context)?.pop<String>(match ? pin : null);
if (!match) {
showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.genericFailureFeedback),
actions: const [OkButton()],
),
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
);
}
} else {
_firstPin = pin;
_controller.clear();
setState(() => _confirming = true);
}
} else {
Navigator.maybeOf(context)?.pop<String>(pin);
}
},
onCompleted: _submit,
animationType: AnimationType.scale,
keyboardType: TextInputType.number,
autoFocus: true,
@ -80,4 +57,29 @@ class _PinDialogState extends State<PinDialog> {
),
);
}
void _submit(String pin) {
if (widget.needConfirmation) {
if (_confirming) {
final match = _firstPin == pin;
Navigator.maybeOf(context)?.pop<String>(match ? pin : null);
if (!match) {
showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.genericFailureFeedback),
actions: const [OkButton()],
),
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
);
}
} else {
_firstPin = pin;
_controller.clear();
setState(() => _confirming = true);
}
} else {
Navigator.maybeOf(context)?.pop<String>(pin);
}
}
}

View file

@ -13,7 +13,7 @@ import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/durations.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/identity/aves_filter_chip.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/services/common/services.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/reselectable_radio_list_tile.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.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/app_bar/app_bar_subtitle.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/identity/aves_app_bar.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/theme/colors.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/scaffold.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 Widget Function() emptyBuilder;
final HeroType heroType;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
FilterGridPage({
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 showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>(
return NotificationListener<DraggableScrollbarNotification>(
onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event);
return false;

View file

@ -229,9 +229,12 @@ class _HomePageState extends State<HomePage> {
}
}
break;
case AppMode.setWallpaper:
// for video playback storage
await metadataDb.init();
break;
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
case AppMode.setWallpaper:
case AppMode.slideshow:
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/entry_set_action_delegate.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/behaviour/routes.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/map/map_info_row.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:collection/collection.dart';
import 'package:flutter/material.dart';

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