Merge branch 'develop'
This commit is contained in:
commit
7b2f72cf14
153 changed files with 2414 additions and 1369 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit c07f7888888435fd9df505aa2efc38d3cf65681b
|
||||
Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" -> {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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 -->
|
||||
|
|
5
fastlane/metadata/android/en-US/changelogs/94.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/94.txt
Normal 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
|
5
fastlane/metadata/android/en-US/changelogs/9401.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/9401.txt
Normal 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
|
|
@ -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>.
|
|
@ -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,
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1262,5 +1262,17 @@
|
|||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"exportEntryDialogWriteMetadata": "메타데이터 저장",
|
||||
"@exportEntryDialogWriteMetadata": {}
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"patternDialogConfirm": "패턴을 확인하세요",
|
||||
"@patternDialogConfirm": {},
|
||||
"patternDialogEnter": "패턴을 입력하세요",
|
||||
"@patternDialogEnter": {},
|
||||
"vaultLockTypePattern": "패턴",
|
||||
"@vaultLockTypePattern": {},
|
||||
"settingsVideoEnablePip": "PIP (화면 속 화면)",
|
||||
"@settingsVideoEnablePip": {},
|
||||
"settingsVideoBackgroundMode": "백그라운드 재생",
|
||||
"@settingsVideoBackgroundMode": {},
|
||||
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
|
||||
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1420,5 +1420,17 @@
|
|||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {}
|
||||
"@lengthUnitPercent": {},
|
||||
"settingsVideoEnablePip": "Картинка в картинці",
|
||||
"@settingsVideoEnablePip": {},
|
||||
"vaultLockTypePattern": "Шаблон",
|
||||
"@vaultLockTypePattern": {},
|
||||
"patternDialogEnter": "Введіть шаблон",
|
||||
"@patternDialogEnter": {},
|
||||
"patternDialogConfirm": "Підтвердіть шаблон",
|
||||
"@patternDialogConfirm": {},
|
||||
"settingsVideoBackgroundMode": "Фоновий режим",
|
||||
"@settingsVideoBackgroundMode": {},
|
||||
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
|
||||
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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:
|
||||
|
|
15
lib/model/settings/enums/video_background_mode.dart
Normal file
15
lib/model/settings/enums/video_background_mode.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'),
|
||||
);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,18 +191,16 @@ 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(
|
||||
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,
|
||||
|
@ -245,9 +244,21 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
// handle Android TV remote `select` button
|
||||
// handle Android TV remote `select` button (KEYCODE_DPAD_CENTER)
|
||||
// the following keys are already handled by default:
|
||||
// KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER
|
||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||
},
|
||||
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,
|
||||
|
@ -266,6 +277,11 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
supportedLocales: AvesApp.supportedLocales,
|
||||
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
||||
scrollBehavior: StretchMaterialScrollBehavior(),
|
||||
useInheritedMediaQuery: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -275,12 +291,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -354,7 +364,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
|
||||
@override
|
||||
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(
|
||||
|
|
|
@ -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,10 +407,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
},
|
||||
),
|
||||
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
|
||||
PopupMenuItem<EntrySetAction>(
|
||||
enabled: hasSelection,
|
||||
padding: EdgeInsets.zero,
|
||||
child: PopupMenuItemExpansionPanel<EntrySetAction>(
|
||||
PopupMenuExpansionPanel<EntrySetAction>(
|
||||
enabled: hasSelection,
|
||||
value: 'edit',
|
||||
icon: AIcons.edit,
|
||||
|
@ -418,7 +417,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return [
|
||||
|
@ -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,11 +556,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
),
|
||||
);
|
||||
|
||||
return PopupMenuItem(
|
||||
child: TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
return PopupMenuItemContainer(
|
||||
child: Row(
|
||||
children: [
|
||||
buildDivider(),
|
||||
|
@ -574,7 +568,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
buildDivider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +157,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
|||
tileAnimationDelay = Duration.zero;
|
||||
}
|
||||
|
||||
return StreamBuilder(
|
||||
return NotificationListener<OpenViewerNotification>(
|
||||
onNotification: (notification) {
|
||||
_goToViewer(collection, notification.entry);
|
||||
return true;
|
||||
},
|
||||
child: StreamBuilder(
|
||||
stream: source.eventBus.on<AspectRatioChangedEvent>(),
|
||||
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
|
@ -205,6 +215,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
|||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
|
@ -227,6 +238,36 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _goToViewer(CollectionLens collection, AvesEntry entry) {
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
Navigator.maybeOf(context)?.push(
|
||||
TransparentMaterialPageRoute(
|
||||
settings: const RouteSettings(name: EntryViewerPage.routeName),
|
||||
pageBuilder: (context, a, sa) {
|
||||
final viewerCollection = collection.copyWith(
|
||||
listenToSource: false,
|
||||
);
|
||||
Widget child = EntryViewerPage(
|
||||
collection: viewerCollection,
|
||||
initialEntry: entry,
|
||||
);
|
||||
|
||||
if (selection.isSelecting) {
|
||||
child = MultiProvider(
|
||||
providers: [
|
||||
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickMediaInternal)),
|
||||
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CollectionSectionedContent extends StatefulWidget {
|
||||
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 }
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,7 +216,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
),
|
||||
);
|
||||
}),
|
||||
RepaintBoundary(
|
||||
// exclude semantics, otherwise this layer will block access to content layers below when using TalkBack
|
||||
ExcludeSemantics(
|
||||
child: RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onLongPressStart: (details) {
|
||||
_longPressLastGlobalPosition = details.globalPosition;
|
||||
|
@ -275,6 +254,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -304,7 +284,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
}
|
||||
|
||||
void _onVerticalDragStart() {
|
||||
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 }
|
31
lib/widgets/common/basic/draggable_scrollbar/transition.dart
Normal file
31
lib/widgets/common/basic/draggable_scrollbar/transition.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
35
lib/widgets/common/basic/popup/container.dart
Normal file
35
lib/widgets/common/basic/popup/container.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,8 +57,13 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
|
|||
elevation: 0,
|
||||
children: [
|
||||
ExpansionPanel(
|
||||
headerBuilder: (context, isExpanded) => DefaultTextStyle(
|
||||
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(
|
||||
|
@ -113,6 +72,8 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
|
|||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
55
lib/widgets/common/basic/popup/menu_row.dart
Normal file
55
lib/widgets/common/basic/popup/menu_row.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -22,28 +22,13 @@ 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(
|
||||
key: ValueKey(isSelected),
|
||||
icon: isSelected ? AIcons.selected : AIcons.unselected,
|
||||
margin: EdgeInsets.zero,
|
||||
)
|
||||
: const SizedBox();
|
||||
child = AnimatedSwitcher(
|
||||
return AnimatedSwitcher(
|
||||
duration: duration,
|
||||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeOutBack,
|
||||
transitionBuilder: (child, animation) => ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
child = AnimatedContainer(
|
||||
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(
|
||||
|
@ -51,15 +36,24 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
|
|||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
return child;
|
||||
},
|
||||
)
|
||||
: const SizedBox();
|
||||
return AnimatedSwitcher(
|
||||
duration: duration,
|
||||
child: child,
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
break;
|
||||
case MapNavigationButton.none:
|
||||
break;
|
||||
}
|
||||
|
||||
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
||||
|
|
16
lib/widgets/common/providers/durations_provider.dart
Normal file
16
lib/widgets/common/providers/durations_provider.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
}
|
|
@ -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>(
|
||||
super.child,
|
||||
}) : super(
|
||||
create: (context) => HighlightInfo(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
|
|
9
lib/widgets/common/thumbnail/notifications.dart
Normal file
9
lib/widgets/common/thumbnail/notifications.dart
Normal 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);
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -41,7 +41,16 @@ class _PasswordDialogState extends State<PasswordDialog> {
|
|||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
obscureText: true,
|
||||
onSubmitted: (password) {
|
||||
onSubmitted: _submit,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit(String password) {
|
||||
if (widget.needConfirmation) {
|
||||
if (_confirming) {
|
||||
final match = _firstPassword == password;
|
||||
|
@ -65,12 +74,5 @@ class _PasswordDialogState extends State<PasswordDialog> {
|
|||
} else {
|
||||
Navigator.maybeOf(context)?.pop<String>(password);
|
||||
}
|
||||
},
|
||||
autofillHints: const [AutofillHints.password],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
75
lib/widgets/dialogs/filter_editors/pattern_dialog.dart
Normal file
75
lib/widgets/dialogs/filter_editors/pattern_dialog.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,27 @@ class _PinDialogState extends State<PinDialog> {
|
|||
controller: _controller,
|
||||
obscureText: true,
|
||||
onChanged: (v) {},
|
||||
onCompleted: (pin) {
|
||||
onCompleted: _submit,
|
||||
animationType: AnimationType.scale,
|
||||
keyboardType: TextInputType.number,
|
||||
autoFocus: true,
|
||||
autoDismissKeyboard: !widget.needConfirmation || _confirming,
|
||||
pinTheme: PinTheme(
|
||||
activeColor: colorScheme.onBackground,
|
||||
inactiveColor: colorScheme.onBackground,
|
||||
selectedColor: colorScheme.secondary,
|
||||
selectedFillColor: colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
shape: PinCodeFieldShape.box,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit(String pin) {
|
||||
if (widget.needConfirmation) {
|
||||
if (_confirming) {
|
||||
final match = _firstPin == pin;
|
||||
|
@ -61,23 +81,5 @@ class _PinDialogState extends State<PinDialog> {
|
|||
} else {
|
||||
Navigator.maybeOf(context)?.pop<String>(pin);
|
||||
}
|
||||
},
|
||||
animationType: AnimationType.scale,
|
||||
keyboardType: TextInputType.number,
|
||||
autoFocus: true,
|
||||
autoDismissKeyboard: !widget.needConfirmation || _confirming,
|
||||
pinTheme: PinTheme(
|
||||
activeColor: colorScheme.onBackground,
|
||||
inactiveColor: colorScheme.onBackground,
|
||||
selectedColor: colorScheme.secondary,
|
||||
selectedFillColor: colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
shape: PinCodeFieldShape.box,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue