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="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
## <a id="v1.8.3"></a>[v1.8.3] - 2023-03-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Collection: preview button when selecting items
|
||||||
|
- Collection: item size in list layout
|
||||||
|
- Vaults: custom pattern lock
|
||||||
|
- Video: picture-in-picture
|
||||||
|
- Video: handle skip next/previous media buttons
|
||||||
|
- TV: more media controls
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- scroll to show item when navigating from Info page
|
||||||
|
- upgraded Flutter to stable v3.7.7
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Accessibility: using accessibility services keeping snack bar beyond countdown
|
||||||
|
- Accessibility: navigation with TalkBack
|
||||||
|
- Vaults: crash when using fingerprint on older Android versions
|
||||||
|
- Vaults: sharing multiple items
|
||||||
|
|
||||||
## <a id="v1.8.2"></a>[v1.8.2] - 2023-02-28
|
## <a id="v1.8.2"></a>[v1.8.2] - 2023-02-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -67,6 +67,16 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- necessary to resolve image editor apps that are not visible in the launcher -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent>
|
||||||
|
<!-- necessary to resolve video editor apps that are not visible in the launcher -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<data android:mimeType="video/*" />
|
||||||
|
</intent>
|
||||||
<!--
|
<!--
|
||||||
from Android 11, `url_launcher` method `canLaunchUrl()` will return false,
|
from Android 11, `url_launcher` method `canLaunchUrl()` will return false,
|
||||||
if appropriate intents are not declared, cf https://pub.dev/packages/url_launcher#configuration=
|
if appropriate intents are not declared, cf https://pub.dev/packages/url_launcher#configuration=
|
||||||
|
@ -96,6 +106,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:theme="@style/NormalTheme"
|
android:theme="@style/NormalTheme"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -283,7 +294,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<!-- as of Flutter v3.3.0, background blur & icon shading fail with Impeller -->
|
<!-- as of Flutter v3.7.7, background blur yields black screen with Impeller -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
android:value="false" />
|
android:value="false" />
|
||||||
|
|
|
@ -13,17 +13,20 @@ import deckers.thibault.aves.channel.calls.*
|
||||||
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
|
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
|
||||||
import deckers.thibault.aves.channel.calls.window.WindowHandler
|
import deckers.thibault.aves.channel.calls.window.WindowHandler
|
||||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||||
|
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
|
||||||
import deckers.thibault.aves.utils.FlutterUtils
|
import deckers.thibault.aves.utils.FlutterUtils
|
||||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class WallpaperActivity : FlutterFragmentActivity() {
|
class WallpaperActivity : FlutterFragmentActivity() {
|
||||||
private lateinit var intentDataMap: MutableMap<String, Any?>
|
private lateinit var intentDataMap: MutableMap<String, Any?>
|
||||||
|
private lateinit var mediaSessionHandler: MediaSessionHandler
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (FlutterUtils.isSoftwareRenderingRequired()) {
|
if (FlutterUtils.isSoftwareRenderingRequired()) {
|
||||||
|
@ -42,12 +45,19 @@ class WallpaperActivity : FlutterFragmentActivity() {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
val messenger = flutterEngine.dartExecutor
|
val messenger = flutterEngine.dartExecutor
|
||||||
|
|
||||||
|
// notification: platform -> dart
|
||||||
|
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
|
||||||
|
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
|
||||||
|
}
|
||||||
|
|
||||||
// dart -> platform -> dart
|
// dart -> platform -> dart
|
||||||
// - need Context
|
// - need Context
|
||||||
|
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
|
||||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||||
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
|
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
|
||||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
||||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
||||||
|
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
|
||||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||||
// - need ContextWrapper
|
// - need ContextWrapper
|
||||||
|
@ -79,6 +89,11 @@ class WallpaperActivity : FlutterFragmentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mediaSessionHandler.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getIntentData" -> {
|
"getIntentData" -> {
|
||||||
|
|
|
@ -285,7 +285,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) })
|
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { getShareableUri(context, Uri.parse(it)) })
|
||||||
val mimeTypes = urisByMimeType.keys.toTypedArray()
|
val mimeTypes = urisByMimeType.keys.toTypedArray()
|
||||||
|
|
||||||
// simplify share intent for a single item, as some apps can handle one item but not more
|
// simplify share intent for a single item, as some apps can handle one item but not more
|
||||||
|
@ -296,7 +296,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
Intent(Intent.ACTION_SEND)
|
Intent(Intent.ACTION_SEND)
|
||||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.setType(mimeType)
|
.setType(mimeType)
|
||||||
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
|
.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
} else {
|
} else {
|
||||||
var mimeType = "*/*"
|
var mimeType = "*/*"
|
||||||
if (mimeTypes.size == 1) {
|
if (mimeTypes.size == 1) {
|
||||||
|
|
|
@ -65,11 +65,13 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
||||||
val stateString = call.argument<String>("state")
|
val stateString = call.argument<String>("state")
|
||||||
val positionMillis = call.argument<Number>("positionMillis")?.toLong()
|
val positionMillis = call.argument<Number>("positionMillis")?.toLong()
|
||||||
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
|
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
|
||||||
|
val canSkipToNext = call.argument<Boolean>("canSkipToNext")
|
||||||
|
val canSkipToPrevious = call.argument<Boolean>("canSkipToPrevious")
|
||||||
|
|
||||||
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) {
|
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null || canSkipToNext == null || canSkipToPrevious == null) {
|
||||||
result.error(
|
result.error(
|
||||||
"updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" +
|
"updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" +
|
||||||
", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed", null
|
", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed, canSkipToNext=$canSkipToNext, canSkipToPrevious=$canSkipToPrevious", null
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -90,6 +92,12 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
||||||
} else {
|
} else {
|
||||||
actions or PlaybackStateCompat.ACTION_PLAY
|
actions or PlaybackStateCompat.ACTION_PLAY
|
||||||
}
|
}
|
||||||
|
if (canSkipToNext) {
|
||||||
|
actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||||
|
}
|
||||||
|
if (canSkipToPrevious) {
|
||||||
|
actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||||
|
}
|
||||||
|
|
||||||
val playbackState = PlaybackStateCompat.Builder()
|
val playbackState = PlaybackStateCompat.Builder()
|
||||||
.setState(
|
.setState(
|
||||||
|
|
|
@ -46,6 +46,16 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
|
||||||
success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE))
|
success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSkipToNext() {
|
||||||
|
super.onSkipToNext()
|
||||||
|
success(hashMapOf(KEY_COMMAND to COMMAND_SKIP_TO_NEXT))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipToPrevious() {
|
||||||
|
super.onSkipToPrevious()
|
||||||
|
success(hashMapOf(KEY_COMMAND to COMMAND_SKIP_TO_PREVIOUS))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
|
success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
|
||||||
|
@ -70,6 +80,8 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
|
||||||
|
|
||||||
const val COMMAND_PLAY = "play"
|
const val COMMAND_PLAY = "play"
|
||||||
const val COMMAND_PAUSE = "pause"
|
const val COMMAND_PAUSE = "pause"
|
||||||
|
const val COMMAND_SKIP_TO_NEXT = "skip_to_next"
|
||||||
|
const val COMMAND_SKIP_TO_PREVIOUS = "skip_to_previous"
|
||||||
const val COMMAND_STOP = "stop"
|
const val COMMAND_STOP = "stop"
|
||||||
const val COMMAND_SEEK = "seek"
|
const val COMMAND_SEEK = "seek"
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,14 @@ object StorageUtils {
|
||||||
|
|
||||||
const val TRASH_PATH_PLACEHOLDER = "#trash"
|
const val TRASH_PATH_PLACEHOLDER = "#trash"
|
||||||
|
|
||||||
|
// whether the provided path is on one of this app specific directories:
|
||||||
|
// - /storage/{volume}/Android/data/{package_name}/files
|
||||||
|
// - /data/user/0/{package_name}/files
|
||||||
private fun isAppFile(context: Context, path: String): Boolean {
|
private fun isAppFile(context: Context, path: String): Boolean {
|
||||||
val dirs = context.getExternalFilesDirs(null).filterNotNull()
|
val dirs = listOf(
|
||||||
|
*context.getExternalFilesDirs(null).filterNotNull().toTypedArray(),
|
||||||
|
context.filesDir,
|
||||||
|
)
|
||||||
return dirs.any { path.startsWith(it.path) }
|
return dirs.any { path.startsWith(it.path) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
<style name="NormalTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
|
||||||
<!-- API28+, draws next to the notch in fullscreen -->
|
<!-- API28+, draws next to the notch in fullscreen -->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
|
||||||
<!-- API28+, draws next to the notch in fullscreen -->
|
<!-- API28+, draws next to the notch in fullscreen -->
|
||||||
|
|
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>. Пользователи могут легко переходить от альбомов к фотографиям, тэгам, картам и т.д.
|
<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,
|
AppMode.pickMultipleMediaExternal,
|
||||||
}.contains(this);
|
}.contains(this);
|
||||||
|
|
||||||
|
bool get canEditEntry => {
|
||||||
|
AppMode.main,
|
||||||
|
AppMode.view,
|
||||||
|
}.contains(this);
|
||||||
|
|
||||||
bool get canSelectMedia => {
|
bool get canSelectMedia => {
|
||||||
AppMode.main,
|
AppMode.main,
|
||||||
AppMode.pickMultipleMediaExternal,
|
AppMode.pickMultipleMediaExternal,
|
||||||
|
|
|
@ -1366,5 +1366,65 @@
|
||||||
"settingsViewerShowDescription": "Zobrazit popis",
|
"settingsViewerShowDescription": "Zobrazit popis",
|
||||||
"@settingsViewerShowDescription": {},
|
"@settingsViewerShowDescription": {},
|
||||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti",
|
"settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti",
|
||||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||||
|
"settingsDisablingBinWarningDialogMessage": "Položky v koši budou navždy odstraněny.",
|
||||||
|
"@settingsDisablingBinWarningDialogMessage": {},
|
||||||
|
"chipActionLock": "Uzamknout",
|
||||||
|
"@chipActionLock": {},
|
||||||
|
"chipActionGoToPlacePage": "Zobrazit v místech",
|
||||||
|
"@chipActionGoToPlacePage": {},
|
||||||
|
"vaultLockTypePassword": "Heslo",
|
||||||
|
"@vaultLockTypePassword": {},
|
||||||
|
"settingsConfirmationVaultDataLoss": "Zobrazit varování o možnosti ztráty dat trezorů",
|
||||||
|
"@settingsConfirmationVaultDataLoss": {},
|
||||||
|
"lengthUnitPercent": "%",
|
||||||
|
"@lengthUnitPercent": {},
|
||||||
|
"albumTierVaults": "Trezory",
|
||||||
|
"@albumTierVaults": {},
|
||||||
|
"lengthUnitPixel": "px",
|
||||||
|
"@lengthUnitPixel": {},
|
||||||
|
"vaultLockTypePin": "PIN",
|
||||||
|
"@vaultLockTypePin": {},
|
||||||
|
"chipActionCreateVault": "Vytvořit trezor",
|
||||||
|
"@chipActionCreateVault": {},
|
||||||
|
"chipActionConfigureVault": "Upravit trezor",
|
||||||
|
"@chipActionConfigureVault": {},
|
||||||
|
"newVaultWarningDialogMessage": "Položky v trezorech jsou přístupné pouze této aplikaci a žádné jiné.\n\nPokud tuto aplikaci odinstalujete, nebo smažete její data, o všechny tyto položky přijdete.",
|
||||||
|
"@newVaultWarningDialogMessage": {},
|
||||||
|
"vaultLockTypePattern": "Vzor",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"vaultDialogLockTypeLabel": "Typ uzamčení",
|
||||||
|
"@vaultDialogLockTypeLabel": {},
|
||||||
|
"pinDialogEnter": "Zadejte PIN",
|
||||||
|
"@pinDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Potvrďte gesto",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"patternDialogEnter": "Zadejte gesto",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"pinDialogConfirm": "Potvrďte PIN",
|
||||||
|
"@pinDialogConfirm": {},
|
||||||
|
"passwordDialogEnter": "Zadejte heslo",
|
||||||
|
"@passwordDialogEnter": {},
|
||||||
|
"passwordDialogConfirm": "Potvrďte heslo",
|
||||||
|
"@passwordDialogConfirm": {},
|
||||||
|
"exportEntryDialogWriteMetadata": "Zapsat metadata",
|
||||||
|
"@exportEntryDialogWriteMetadata": {},
|
||||||
|
"drawerPlacePage": "Místa",
|
||||||
|
"@drawerPlacePage": {},
|
||||||
|
"placePageTitle": "Místa",
|
||||||
|
"@placePageTitle": {},
|
||||||
|
"placeEmpty": "Žádná místa",
|
||||||
|
"@placeEmpty": {},
|
||||||
|
"authenticateToConfigureVault": "Pro úpravu teroru se ověřte",
|
||||||
|
"@authenticateToConfigureVault": {},
|
||||||
|
"authenticateToUnlockVault": "Ověřte se pro otevření trezoru",
|
||||||
|
"@authenticateToUnlockVault": {},
|
||||||
|
"newVaultDialogTitle": "Nový trezor",
|
||||||
|
"@newVaultDialogTitle": {},
|
||||||
|
"configureVaultDialogTitle": "Nastavit trezor",
|
||||||
|
"@configureVaultDialogTitle": {},
|
||||||
|
"vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje",
|
||||||
|
"@vaultDialogLockModeWhenScreenOff": {},
|
||||||
|
"vaultBinUsageDialogMessage": "Některé trezory používají koš.",
|
||||||
|
"@vaultBinUsageDialogMessage": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,9 +226,12 @@
|
||||||
"unitSystemMetric": "Metric",
|
"unitSystemMetric": "Metric",
|
||||||
"unitSystemImperial": "Imperial",
|
"unitSystemImperial": "Imperial",
|
||||||
|
|
||||||
"vaultLockTypePin": "Pin",
|
"vaultLockTypePattern": "Pattern",
|
||||||
|
"vaultLockTypePin": "PIN",
|
||||||
"vaultLockTypePassword": "Password",
|
"vaultLockTypePassword": "Password",
|
||||||
|
|
||||||
|
"settingsVideoEnablePip": "Picture-in-picture",
|
||||||
|
|
||||||
"videoControlsPlay": "Play",
|
"videoControlsPlay": "Play",
|
||||||
"videoControlsPlaySeek": "Play & seek backward/forward",
|
"videoControlsPlaySeek": "Play & seek backward/forward",
|
||||||
"videoControlsPlayOutside": "Open with other player",
|
"videoControlsPlayOutside": "Open with other player",
|
||||||
|
@ -384,8 +387,11 @@
|
||||||
"vaultDialogLockModeWhenScreenOff": "Lock when screen turns off",
|
"vaultDialogLockModeWhenScreenOff": "Lock when screen turns off",
|
||||||
"vaultDialogLockTypeLabel": "Lock type",
|
"vaultDialogLockTypeLabel": "Lock type",
|
||||||
|
|
||||||
"pinDialogEnter": "Enter pin",
|
"patternDialogEnter": "Enter pattern",
|
||||||
"pinDialogConfirm": "Confirm pin",
|
"patternDialogConfirm": "Confirm pattern",
|
||||||
|
|
||||||
|
"pinDialogEnter": "Enter PIN",
|
||||||
|
"pinDialogConfirm": "Confirm PIN",
|
||||||
|
|
||||||
"passwordDialogEnter": "Enter password",
|
"passwordDialogEnter": "Enter password",
|
||||||
"passwordDialogConfirm": "Confirm password",
|
"passwordDialogConfirm": "Confirm password",
|
||||||
|
@ -792,6 +798,8 @@
|
||||||
"settingsVideoAutoPlay": "Auto play",
|
"settingsVideoAutoPlay": "Auto play",
|
||||||
"settingsVideoLoopModeTile": "Loop mode",
|
"settingsVideoLoopModeTile": "Loop mode",
|
||||||
"settingsVideoLoopModeDialogTitle": "Loop Mode",
|
"settingsVideoLoopModeDialogTitle": "Loop Mode",
|
||||||
|
"settingsVideoBackgroundMode": "Background mode",
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Background Mode",
|
||||||
|
|
||||||
"settingsSubtitleThemeTile": "Subtitles",
|
"settingsSubtitleThemeTile": "Subtitles",
|
||||||
"settingsSubtitleThemePageTitle": "Subtitles",
|
"settingsSubtitleThemePageTitle": "Subtitles",
|
||||||
|
|
|
@ -1262,5 +1262,17 @@
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {},
|
"@lengthUnitPercent": {},
|
||||||
"exportEntryDialogWriteMetadata": "Escribir metadatos",
|
"exportEntryDialogWriteMetadata": "Escribir metadatos",
|
||||||
"@exportEntryDialogWriteMetadata": {}
|
"@exportEntryDialogWriteMetadata": {},
|
||||||
|
"vaultLockTypePattern": "Patrón",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"patternDialogEnter": "Introduzca el patrón",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Confirme el patrón",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"settingsVideoEnablePip": "Imagen-en-imagen",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "Reproducción de fondo",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Background mode",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1420,5 +1420,17 @@
|
||||||
"vaultLockTypePassword": "Pasahitza",
|
"vaultLockTypePassword": "Pasahitza",
|
||||||
"@vaultLockTypePassword": {},
|
"@vaultLockTypePassword": {},
|
||||||
"drawerPlacePage": "Lekuak",
|
"drawerPlacePage": "Lekuak",
|
||||||
"@drawerPlacePage": {}
|
"@drawerPlacePage": {},
|
||||||
|
"vaultLockTypePattern": "Patroia",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"patternDialogEnter": "Sartu patroia",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Konfirmatu patroia",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"settingsVideoEnablePip": "Bideoa leihotxoan",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1262,5 +1262,17 @@
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {},
|
"@lengthUnitPercent": {},
|
||||||
"exportEntryDialogWriteMetadata": "Écrire les métadonnées",
|
"exportEntryDialogWriteMetadata": "Écrire les métadonnées",
|
||||||
"@exportEntryDialogWriteMetadata": {}
|
"@exportEntryDialogWriteMetadata": {},
|
||||||
|
"patternDialogEnter": "Entrez votre modèle",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Confirmez votre modèle",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"vaultLockTypePattern": "Modèle",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"settingsVideoEnablePip": "Picture-in-picture",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "Lecture en arrière-plan",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Arrière-plan",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1262,5 +1262,17 @@
|
||||||
"lengthUnitPixel": "px",
|
"lengthUnitPixel": "px",
|
||||||
"@lengthUnitPixel": {},
|
"@lengthUnitPixel": {},
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {}
|
"@lengthUnitPercent": {},
|
||||||
|
"vaultLockTypePattern": "Pola",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"patternDialogConfirm": "Konfirmasi pola",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"patternDialogEnter": "Masukkan pola",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"settingsVideoEnablePip": "Gambar dalam gambar",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "Mode latar belakang",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Mode Latar Belakang",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1262,5 +1262,17 @@
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {},
|
"@lengthUnitPercent": {},
|
||||||
"exportEntryDialogWriteMetadata": "메타데이터 저장",
|
"exportEntryDialogWriteMetadata": "메타데이터 저장",
|
||||||
"@exportEntryDialogWriteMetadata": {}
|
"@exportEntryDialogWriteMetadata": {},
|
||||||
|
"patternDialogConfirm": "패턴을 확인하세요",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"patternDialogEnter": "패턴을 입력하세요",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"vaultLockTypePattern": "패턴",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"settingsVideoEnablePip": "PIP (화면 속 화면)",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "백그라운드 재생",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1366,5 +1366,59 @@
|
||||||
"tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.",
|
"tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.",
|
||||||
"@tooManyItemsErrorDialogMessage": {},
|
"@tooManyItemsErrorDialogMessage": {},
|
||||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke",
|
"settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke",
|
||||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||||
|
"settingsDisablingBinWarningDialogMessage": "Elementer i papirkurven vil bli slettet for godt.",
|
||||||
|
"@settingsDisablingBinWarningDialogMessage": {},
|
||||||
|
"chipActionLock": "Lås",
|
||||||
|
"@chipActionLock": {},
|
||||||
|
"chipActionGoToPlacePage": "Vis i «Steder»",
|
||||||
|
"@chipActionGoToPlacePage": {},
|
||||||
|
"chipActionCreateVault": "Opprett hvelv",
|
||||||
|
"@chipActionCreateVault": {},
|
||||||
|
"chipActionConfigureVault": "Sett opp hvelv",
|
||||||
|
"@chipActionConfigureVault": {},
|
||||||
|
"albumTierVaults": "Hvelv",
|
||||||
|
"@albumTierVaults": {},
|
||||||
|
"lengthUnitPixel": "px",
|
||||||
|
"@lengthUnitPixel": {},
|
||||||
|
"newVaultDialogTitle": "Nytt hvelv",
|
||||||
|
"@newVaultDialogTitle": {},
|
||||||
|
"configureVaultDialogTitle": "Sett opp hvelv",
|
||||||
|
"@configureVaultDialogTitle": {},
|
||||||
|
"pinDialogEnter": "Skriv inn PIN",
|
||||||
|
"@pinDialogEnter": {},
|
||||||
|
"pinDialogConfirm": "Bekreft PIN",
|
||||||
|
"@pinDialogConfirm": {},
|
||||||
|
"passwordDialogEnter": "Skriv inn passord",
|
||||||
|
"@passwordDialogEnter": {},
|
||||||
|
"passwordDialogConfirm": "Bekreft passord",
|
||||||
|
"@passwordDialogConfirm": {},
|
||||||
|
"authenticateToConfigureVault": "Identitetsbekreft for å sette opp hvelv",
|
||||||
|
"@authenticateToConfigureVault": {},
|
||||||
|
"authenticateToUnlockVault": "Identitetsbekreft for å låse opp hvelv",
|
||||||
|
"@authenticateToUnlockVault": {},
|
||||||
|
"vaultBinUsageDialogMessage": "Noen hvelv bruker papirkurven.",
|
||||||
|
"@vaultBinUsageDialogMessage": {},
|
||||||
|
"newVaultWarningDialogMessage": "Elementer i hvelv er kun tilgjengelig for dette programmet, og ikke andre.\n\nHvis du avinstallerer dette programmet, eller tømmer denne programdataen vil du miste alle disse elementene.",
|
||||||
|
"@newVaultWarningDialogMessage": {},
|
||||||
|
"drawerPlacePage": "Steder",
|
||||||
|
"@drawerPlacePage": {},
|
||||||
|
"vaultLockTypePin": "PIN",
|
||||||
|
"@vaultLockTypePin": {},
|
||||||
|
"vaultLockTypePassword": "Passord",
|
||||||
|
"@vaultLockTypePassword": {},
|
||||||
|
"exportEntryDialogWriteMetadata": "Skriv metadata",
|
||||||
|
"@exportEntryDialogWriteMetadata": {},
|
||||||
|
"placeEmpty": "Ingen steder",
|
||||||
|
"@placeEmpty": {},
|
||||||
|
"vaultDialogLockModeWhenScreenOff": "Lås når skjermen skrur seg av",
|
||||||
|
"@vaultDialogLockModeWhenScreenOff": {},
|
||||||
|
"vaultDialogLockTypeLabel": "Låsetype",
|
||||||
|
"@vaultDialogLockTypeLabel": {},
|
||||||
|
"placePageTitle": "Steder",
|
||||||
|
"@placePageTitle": {},
|
||||||
|
"settingsConfirmationVaultDataLoss": "Vis advarsel om hvelv-datatap",
|
||||||
|
"@settingsConfirmationVaultDataLoss": {},
|
||||||
|
"lengthUnitPercent": "%",
|
||||||
|
"@lengthUnitPercent": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1420,5 +1420,17 @@
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {},
|
"@lengthUnitPercent": {},
|
||||||
"placePageTitle": "Miejsca",
|
"placePageTitle": "Miejsca",
|
||||||
"@placePageTitle": {}
|
"@placePageTitle": {},
|
||||||
|
"vaultLockTypePattern": "Wzór",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"patternDialogEnter": "Ustaw wzór",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Potwierdź wzór",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"settingsVideoEnablePip": "Obraz w obrazie",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "Tryb tła",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Tryb tła",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1216,5 +1216,61 @@
|
||||||
"pinDialogConfirm": "Confirme o PIN",
|
"pinDialogConfirm": "Confirme o PIN",
|
||||||
"@pinDialogConfirm": {},
|
"@pinDialogConfirm": {},
|
||||||
"passwordDialogEnter": "Digite a senha",
|
"passwordDialogEnter": "Digite a senha",
|
||||||
"@passwordDialogEnter": {}
|
"@passwordDialogEnter": {},
|
||||||
|
"columnCount": "{count, plural, =1{1 coluna} other{{count} colunas}}",
|
||||||
|
"@columnCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settingsDisablingBinWarningDialogMessage": "Os itens na lixeira serão excluídos para sempre.",
|
||||||
|
"@settingsDisablingBinWarningDialogMessage": {},
|
||||||
|
"vaultLockTypePin": "PIN",
|
||||||
|
"@vaultLockTypePin": {},
|
||||||
|
"chipActionCreateVault": "Criar cofre",
|
||||||
|
"@chipActionCreateVault": {},
|
||||||
|
"chipActionConfigureVault": "Configurar cofre",
|
||||||
|
"@chipActionConfigureVault": {},
|
||||||
|
"albumTierVaults": "Cofres",
|
||||||
|
"@albumTierVaults": {},
|
||||||
|
"lengthUnitPixel": "pixels",
|
||||||
|
"@lengthUnitPixel": {},
|
||||||
|
"lengthUnitPercent": "%",
|
||||||
|
"@lengthUnitPercent": {},
|
||||||
|
"exportEntryDialogWriteMetadata": "Escrever metadados",
|
||||||
|
"@exportEntryDialogWriteMetadata": {},
|
||||||
|
"chipActionGoToPlacePage": "Exibir em Lugares",
|
||||||
|
"@chipActionGoToPlacePage": {},
|
||||||
|
"placePageTitle": "Lugares",
|
||||||
|
"@placePageTitle": {},
|
||||||
|
"drawerPlacePage": "Lugares",
|
||||||
|
"@drawerPlacePage": {},
|
||||||
|
"placeEmpty": "Sem lugares",
|
||||||
|
"@placeEmpty": {},
|
||||||
|
"tooManyItemsErrorDialogMessage": "Tente novamente com menos itens.",
|
||||||
|
"@tooManyItemsErrorDialogMessage": {},
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume": "Deslize para cima ou para baixo para ajustar o brilho/volume",
|
||||||
|
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||||
|
"passwordDialogConfirm": "Confirmar senha",
|
||||||
|
"@passwordDialogConfirm": {},
|
||||||
|
"authenticateToConfigureVault": "Autentique-se para configurar o cofre",
|
||||||
|
"@authenticateToConfigureVault": {},
|
||||||
|
"authenticateToUnlockVault": "Autentique-se para desbloquear o cofre",
|
||||||
|
"@authenticateToUnlockVault": {},
|
||||||
|
"vaultBinUsageDialogMessage": "Alguns cofres estão usando a lixeira.",
|
||||||
|
"@vaultBinUsageDialogMessage": {},
|
||||||
|
"newVaultWarningDialogMessage": "Os itens nos cofres estão disponíveis apenas para este aplicativo e nenhum outro.\n\nSe você desinstalar este aplicativo ou limpar os dados do aplicativo, perderá todos esses itens.",
|
||||||
|
"@newVaultWarningDialogMessage": {},
|
||||||
|
"settingsConfirmationVaultDataLoss": "Mostrar aviso de perda de dados do cofre",
|
||||||
|
"@settingsConfirmationVaultDataLoss": {},
|
||||||
|
"patternDialogEnter": "Inserir padrão",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Confirmar padrão",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"vaultLockTypePattern": "Padrão",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"settingsVideoEnablePip": "Picture-in-picture",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"settingsVideoBackgroundMode": "Modo background",
|
||||||
|
"@settingsVideoBackgroundMode": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1230,5 +1230,19 @@
|
||||||
"albumTierVaults": "Хранилища",
|
"albumTierVaults": "Хранилища",
|
||||||
"@albumTierVaults": {},
|
"@albumTierVaults": {},
|
||||||
"newVaultWarningDialogMessage": "Элементы внутри хранилищ доступны только для этого приложения, и никакого другого.\n\nЕсли вы удалите приложение или очистите его данные, то вы потеряете все содержимое внутри хранилищ.",
|
"newVaultWarningDialogMessage": "Элементы внутри хранилищ доступны только для этого приложения, и никакого другого.\n\nЕсли вы удалите приложение или очистите его данные, то вы потеряете все содержимое внутри хранилищ.",
|
||||||
"@newVaultWarningDialogMessage": {}
|
"@newVaultWarningDialogMessage": {},
|
||||||
|
"filterLocatedLabel": "С местоположением",
|
||||||
|
"@filterLocatedLabel": {},
|
||||||
|
"filterTaggedLabel": "С тэгами",
|
||||||
|
"@filterTaggedLabel": {},
|
||||||
|
"chipActionGoToPlacePage": "Показать в местах",
|
||||||
|
"@chipActionGoToPlacePage": {},
|
||||||
|
"settingsModificationWarningDialogMessage": "Другие настройки будут изменены.",
|
||||||
|
"@settingsModificationWarningDialogMessage": {},
|
||||||
|
"placePageTitle": "Локации",
|
||||||
|
"@placePageTitle": {},
|
||||||
|
"settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.",
|
||||||
|
"@settingsDisablingBinWarningDialogMessage": {},
|
||||||
|
"lengthUnitPixel": "пикс.",
|
||||||
|
"@lengthUnitPixel": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1420,5 +1420,17 @@
|
||||||
"lengthUnitPixel": "px",
|
"lengthUnitPixel": "px",
|
||||||
"@lengthUnitPixel": {},
|
"@lengthUnitPixel": {},
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {}
|
"@lengthUnitPercent": {},
|
||||||
|
"settingsVideoEnablePip": "Картинка в картинці",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"vaultLockTypePattern": "Шаблон",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"patternDialogEnter": "Введіть шаблон",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Підтвердіть шаблон",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"settingsVideoBackgroundMode": "Фоновий режим",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:floating/floating.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
@ -9,7 +11,7 @@ class Device {
|
||||||
late final String _userAgent;
|
late final String _userAgent;
|
||||||
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
|
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
|
||||||
late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
|
late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
|
||||||
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
|
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
|
||||||
|
|
||||||
String get userAgent => _userAgent;
|
String get userAgent => _userAgent;
|
||||||
|
|
||||||
|
@ -41,6 +43,8 @@ class Device {
|
||||||
|
|
||||||
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
|
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
|
||||||
|
|
||||||
|
bool get supportPictureInPicture => _supportPictureInPicture;
|
||||||
|
|
||||||
Device._private();
|
Device._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
@ -53,6 +57,15 @@ class Device {
|
||||||
final auth = LocalAuthentication();
|
final auth = LocalAuthentication();
|
||||||
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
|
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
|
||||||
|
|
||||||
|
final floating = Floating();
|
||||||
|
try {
|
||||||
|
_supportPictureInPicture = await floating.isPipAvailable;
|
||||||
|
} on PlatformException catch (_) {
|
||||||
|
// as of floating v2.0.0, plugin assumes activity and fails when bound via service
|
||||||
|
_supportPictureInPicture = false;
|
||||||
|
}
|
||||||
|
floating.dispose();
|
||||||
|
|
||||||
final capabilities = await deviceService.getCapabilities();
|
final capabilities = await deviceService.getCapabilities();
|
||||||
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
||||||
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
||||||
|
|
|
@ -91,6 +91,7 @@ class SettingsDefaults {
|
||||||
// video
|
// video
|
||||||
static const enableVideoHardwareAcceleration = true;
|
static const enableVideoHardwareAcceleration = true;
|
||||||
static const videoAutoPlayMode = VideoAutoPlayMode.disabled;
|
static const videoAutoPlayMode = VideoAutoPlayMode.disabled;
|
||||||
|
static const videoBackgroundMode = VideoBackgroundMode.disabled;
|
||||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||||
static const videoShowRawTimedText = false;
|
static const videoShowRawTimedText = false;
|
||||||
static const videoControls = VideoControls.play;
|
static const videoControls = VideoControls.play;
|
||||||
|
|
|
@ -28,12 +28,14 @@ enum ThumbnailOverlayTagIcon { tagged, untagged, none }
|
||||||
|
|
||||||
enum UnitSystem { metric, imperial }
|
enum UnitSystem { metric, imperial }
|
||||||
|
|
||||||
|
enum VideoAutoPlayMode { disabled, playMuted, playWithSound }
|
||||||
|
|
||||||
|
enum VideoBackgroundMode { disabled, pip }
|
||||||
|
|
||||||
enum VideoControls { play, playSeek, playOutside, none }
|
enum VideoControls { play, playSeek, playOutside, none }
|
||||||
|
|
||||||
enum VideoLoopMode { never, shortOnly, always }
|
enum VideoLoopMode { never, shortOnly, always }
|
||||||
|
|
||||||
enum VideoAutoPlayMode { disabled, playMuted, playWithSound }
|
|
||||||
|
|
||||||
enum ViewerTransition { slide, parallax, fade, zoomIn, none }
|
enum ViewerTransition { slide, parallax, fade, zoomIn, none }
|
||||||
|
|
||||||
enum WidgetDisplayedItem { random, mostRecent }
|
enum WidgetDisplayedItem { random, mostRecent }
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'enums.dart';
|
import 'enums.dart';
|
||||||
|
|
||||||
extension ExtraSlideshowVideoPlayback on VideoAutoPlayMode {
|
extension ExtraVideoAutoPlayMode on VideoAutoPlayMode {
|
||||||
String getName(BuildContext context) {
|
String getName(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case VideoAutoPlayMode.disabled:
|
case VideoAutoPlayMode.disabled:
|
||||||
|
|
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/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/controller.dart';
|
import 'package:aves/widgets/viewer/controls/controller.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'enums.dart';
|
import 'enums.dart';
|
||||||
|
|
|
@ -133,6 +133,7 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
// video
|
// video
|
||||||
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
||||||
|
static const videoBackgroundModeKey = 'video_background_mode';
|
||||||
static const videoAutoPlayModeKey = 'video_auto_play_mode';
|
static const videoAutoPlayModeKey = 'video_auto_play_mode';
|
||||||
static const videoLoopModeKey = 'video_loop';
|
static const videoLoopModeKey = 'video_loop';
|
||||||
static const videoControlsKey = 'video_controls';
|
static const videoControlsKey = 'video_controls';
|
||||||
|
@ -284,6 +285,7 @@ class Settings extends ChangeNotifier {
|
||||||
viewerGestureSideTapNext = false;
|
viewerGestureSideTapNext = false;
|
||||||
viewerUseCutout = true;
|
viewerUseCutout = true;
|
||||||
viewerMaxBrightness = false;
|
viewerMaxBrightness = false;
|
||||||
|
videoBackgroundMode = VideoBackgroundMode.disabled;
|
||||||
videoControls = VideoControls.none;
|
videoControls = VideoControls.none;
|
||||||
videoGestureDoubleTapTogglePlay = false;
|
videoGestureDoubleTapTogglePlay = false;
|
||||||
videoGestureSideDoubleTapSeek = false;
|
videoGestureSideDoubleTapSeek = false;
|
||||||
|
@ -298,6 +300,9 @@ class Settings extends ChangeNotifier {
|
||||||
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
|
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
|
||||||
_set(viewerUseCutoutKey, null);
|
_set(viewerUseCutoutKey, null);
|
||||||
}
|
}
|
||||||
|
if (videoBackgroundMode == VideoBackgroundMode.pip && !device.supportPictureInPicture) {
|
||||||
|
_set(videoBackgroundModeKey, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// app
|
// app
|
||||||
|
@ -659,6 +664,10 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString());
|
set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString());
|
||||||
|
|
||||||
|
VideoBackgroundMode get videoBackgroundMode => getEnumOrDefault(videoBackgroundModeKey, SettingsDefaults.videoBackgroundMode, VideoBackgroundMode.values);
|
||||||
|
|
||||||
|
set videoBackgroundMode(VideoBackgroundMode newValue) => _set(videoBackgroundModeKey, newValue.toString());
|
||||||
|
|
||||||
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values);
|
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values);
|
||||||
|
|
||||||
set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString());
|
set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString());
|
||||||
|
@ -1117,6 +1126,7 @@ class Settings extends ChangeNotifier {
|
||||||
case tagSortFactorKey:
|
case tagSortFactorKey:
|
||||||
case imageBackgroundKey:
|
case imageBackgroundKey:
|
||||||
case videoAutoPlayModeKey:
|
case videoAutoPlayModeKey:
|
||||||
|
case videoBackgroundModeKey:
|
||||||
case videoLoopModeKey:
|
case videoLoopModeKey:
|
||||||
case videoControlsKey:
|
case videoControlsKey:
|
||||||
case subtitleTextAlignmentKey:
|
case subtitleTextAlignmentKey:
|
||||||
|
|
|
@ -201,7 +201,8 @@ class MediaStoreSource extends CollectionSource {
|
||||||
// so we manually notify change for potential home screen filters
|
// so we manually notify change for potential home screen filters
|
||||||
notifyAlbumsChanged();
|
notifyAlbumsChanged();
|
||||||
|
|
||||||
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedEntries.length} removed');
|
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done');
|
||||||
|
unawaited(reportService.log('Source refresh complete in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${allNewEntries.length} new, ${removedEntries.length} removed'));
|
||||||
},
|
},
|
||||||
onError: (error) => debugPrint('$runtimeType stream error=$error'),
|
onError: (error) => debugPrint('$runtimeType stream error=$error'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum VaultLockType { system, pin, password }
|
enum VaultLockType { system, pattern, pin, password }
|
||||||
|
|
||||||
extension ExtraVaultLockType on VaultLockType {
|
extension ExtraVaultLockType on VaultLockType {
|
||||||
String getText(BuildContext context) {
|
String getText(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case VaultLockType.system:
|
case VaultLockType.system:
|
||||||
return context.l10n.settingsSystemDefault;
|
return context.l10n.settingsSystemDefault;
|
||||||
|
case VaultLockType.pattern:
|
||||||
|
return context.l10n.vaultLockTypePattern;
|
||||||
case VaultLockType.pin:
|
case VaultLockType.pin:
|
||||||
return context.l10n.vaultLockTypePin;
|
return context.l10n.vaultLockTypePin;
|
||||||
case VaultLockType.password:
|
case VaultLockType.password:
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/filter_editors/password_dialog.dart';
|
import 'package:aves/widgets/dialogs/filter_editors/password_dialog.dart';
|
||||||
|
import 'package:aves/widgets/dialogs/filter_editors/pattern_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart';
|
import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -160,6 +161,16 @@ class Vaults extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case VaultLockType.pattern:
|
||||||
|
final pattern = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const PatternDialog(needConfirmation: false),
|
||||||
|
routeSettings: const RouteSettings(name: PatternDialog.routeName),
|
||||||
|
);
|
||||||
|
if (pattern != null) {
|
||||||
|
confirmed = pattern == await securityService.readValue(details.passKey);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case VaultLockType.pin:
|
case VaultLockType.pin:
|
||||||
final pin = await showDialog<String>(
|
final pin = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -211,6 +222,16 @@ class Vaults extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case VaultLockType.pattern:
|
||||||
|
final pattern = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const PatternDialog(needConfirmation: true),
|
||||||
|
routeSettings: const RouteSettings(name: PatternDialog.routeName),
|
||||||
|
);
|
||||||
|
if (pattern != null) {
|
||||||
|
return await securityService.writeValue(details.passKey, pattern);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case VaultLockType.pin:
|
case VaultLockType.pin:
|
||||||
final pin = await showDialog<String>(
|
final pin = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
@ -118,6 +118,11 @@ class PlatformMediaEditService implements MediaEditService {
|
||||||
required String destinationAlbum,
|
required String destinationAlbum,
|
||||||
required NameConflictStrategy nameConflictStrategy,
|
required NameConflictStrategy nameConflictStrategy,
|
||||||
}) {
|
}) {
|
||||||
|
// TODO TLAD remove log when OOMs are inspected
|
||||||
|
entries.where((v) => (v.sizeBytes ?? 0) > 20000000).forEach((entry) {
|
||||||
|
reportService.log('convert large entry=$entry size=${entry.sizeBytes}');
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return _opStream
|
return _opStream
|
||||||
.receiveBroadcastStream(<String, dynamic>{
|
.receiveBroadcastStream(<String, dynamic>{
|
||||||
|
|
|
@ -11,7 +11,11 @@ import 'package:get_it/get_it.dart';
|
||||||
abstract class MediaSessionService {
|
abstract class MediaSessionService {
|
||||||
Stream<MediaCommandEvent> get mediaCommands;
|
Stream<MediaCommandEvent> get mediaCommands;
|
||||||
|
|
||||||
Future<void> update(AvesVideoController controller);
|
Future<void> update({
|
||||||
|
required AvesVideoController controller,
|
||||||
|
required bool canSkipToNext,
|
||||||
|
required bool canSkipToPrevious,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> release();
|
Future<void> release();
|
||||||
}
|
}
|
||||||
|
@ -38,7 +42,11 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>();
|
Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> update(AvesVideoController controller) async {
|
Future<void> update({
|
||||||
|
required AvesVideoController controller,
|
||||||
|
required bool canSkipToNext,
|
||||||
|
required bool canSkipToPrevious,
|
||||||
|
}) async {
|
||||||
final entry = controller.entry;
|
final entry = controller.entry;
|
||||||
try {
|
try {
|
||||||
await _platformObject.invokeMethod('update', <String, dynamic>{
|
await _platformObject.invokeMethod('update', <String, dynamic>{
|
||||||
|
@ -48,6 +56,8 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
'state': _toPlatformState(controller.status),
|
'state': _toPlatformState(controller.status),
|
||||||
'positionMillis': controller.currentPosition,
|
'positionMillis': controller.currentPosition,
|
||||||
'playbackSpeed': controller.speed,
|
'playbackSpeed': controller.speed,
|
||||||
|
'canSkipToNext': canSkipToNext,
|
||||||
|
'canSkipToPrevious': canSkipToPrevious,
|
||||||
});
|
});
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
|
@ -88,6 +98,12 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
case 'pause':
|
case 'pause':
|
||||||
event = const MediaCommandEvent(MediaCommand.pause);
|
event = const MediaCommandEvent(MediaCommand.pause);
|
||||||
break;
|
break;
|
||||||
|
case 'skip_to_next':
|
||||||
|
event = const MediaCommandEvent(MediaCommand.skipToNext);
|
||||||
|
break;
|
||||||
|
case 'skip_to_previous':
|
||||||
|
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
|
||||||
|
break;
|
||||||
case 'stop':
|
case 'stop':
|
||||||
event = const MediaCommandEvent(MediaCommand.stop);
|
event = const MediaCommandEvent(MediaCommand.stop);
|
||||||
break;
|
break;
|
||||||
|
@ -104,7 +120,7 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaCommand { play, pause, stop, seek }
|
enum MediaCommand { play, pause, skipToNext, skipToPrevious, stop, seek }
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class MediaCommandEvent extends Equatable {
|
class MediaCommandEvent extends Equatable {
|
||||||
|
|
|
@ -77,6 +77,11 @@ class PlatformMetadataEditService implements MetadataEditService {
|
||||||
Map<MetadataType, dynamic> metadata, {
|
Map<MetadataType, dynamic> metadata, {
|
||||||
bool autoCorrectTrailerOffset = true,
|
bool autoCorrectTrailerOffset = true,
|
||||||
}) async {
|
}) async {
|
||||||
|
// TODO TLAD remove log when OOMs are inspected
|
||||||
|
if ((entry.sizeBytes ?? 0) > 20000000) {
|
||||||
|
await reportService.log('edit metadata of large entry=$entry size=${entry.sizeBytes}');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{
|
final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{
|
||||||
'entry': entry.toPlatformEntryMap(),
|
'entry': entry.toPlatformEntryMap(),
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class Durations {
|
class Durations {
|
||||||
// Flutter animations (with margin)
|
// Flutter animations (with margin)
|
||||||
|
@ -72,26 +69,6 @@ class Durations {
|
||||||
static const mapIdleDebounceDelay = Duration(milliseconds: 100);
|
static const mapIdleDebounceDelay = Duration(milliseconds: 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DurationsProvider extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const DurationsProvider({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ProxyProvider<Settings, DurationsData>(
|
|
||||||
update: (context, settings, __) {
|
|
||||||
final enabled = settings.accessibilityAnimations.animate;
|
|
||||||
return enabled ? DurationsData() : DurationsData.noAnimation();
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class DurationsData {
|
class DurationsData {
|
||||||
// common animations
|
// common animations
|
||||||
|
|
|
@ -125,6 +125,7 @@ class AIcons {
|
||||||
static const IconData setCover = MdiIcons.imageEditOutline;
|
static const IconData setCover = MdiIcons.imageEditOutline;
|
||||||
static const IconData share = Icons.share_outlined;
|
static const IconData share = Icons.share_outlined;
|
||||||
static const IconData show = Icons.visibility_outlined;
|
static const IconData show = Icons.visibility_outlined;
|
||||||
|
static const IconData showFullscreen = MdiIcons.arrowExpand;
|
||||||
static const IconData slideshow = Icons.slideshow_outlined;
|
static const IconData slideshow = Icons.slideshow_outlined;
|
||||||
static const IconData speed = Icons.speed_outlined;
|
static const IconData speed = Icons.speed_outlined;
|
||||||
static const IconData stats = Icons.donut_small_outlined;
|
static const IconData stats = Icons.donut_small_outlined;
|
||||||
|
|
|
@ -75,6 +75,11 @@ class Dependencies {
|
||||||
license: mit,
|
license: mit,
|
||||||
sourceUrl: 'https://github.com/deckerst/fijkplayer',
|
sourceUrl: 'https://github.com/deckerst/fijkplayer',
|
||||||
),
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Floating',
|
||||||
|
license: mit,
|
||||||
|
sourceUrl: 'https://github.com/wrbl606/floating',
|
||||||
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flutter Display Mode',
|
name: 'Flutter Display Mode',
|
||||||
license: mit,
|
license: mit,
|
||||||
|
@ -260,6 +265,11 @@ class Dependencies {
|
||||||
license: apache2,
|
license: apache2,
|
||||||
sourceUrl: 'https://github.com/zesage/panorama',
|
sourceUrl: 'https://github.com/zesage/panorama',
|
||||||
),
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Pattern Lock',
|
||||||
|
license: apache2,
|
||||||
|
sourceUrl: 'https://github.com/qwert2603/pattern_lock',
|
||||||
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Percent Indicator',
|
name: 'Percent Indicator',
|
||||||
license: bsd2,
|
license: bsd2,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/app_flavor.dart';
|
import 'package:aves/app_flavor.dart';
|
||||||
import 'package:aves/flutter_version.dart';
|
import 'package:aves/flutter_version.dart';
|
||||||
|
@ -142,7 +141,6 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getInfo(BuildContext context) async {
|
Future<String> _getInfo(BuildContext context) async {
|
||||||
final accessibility = window.accessibilityFeatures;
|
|
||||||
final packageInfo = await PackageInfo.fromPlatform();
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
final flavor = context.read<AppFlavor>().toString().split('.')[1];
|
final flavor = context.read<AppFlavor>().toString().split('.')[1];
|
||||||
|
@ -161,7 +159,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
||||||
'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}',
|
'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}',
|
||||||
'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}',
|
'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}',
|
||||||
'Installer: ${packageInfo.installerStore}',
|
'Installer: ${packageInfo.installerStore}',
|
||||||
'Accessibility: accessibleNavigation=${accessibility.accessibleNavigation}, disableAnimations=${accessibility.disableAnimations}',
|
'Error reporting: ${settings.isErrorReportingAllowed}',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/basic/markdown_container.dart';
|
import 'package:aves/widgets/common/basic/markdown_container.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/intents.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -17,7 +16,6 @@ class PolicyPage extends StatefulWidget {
|
||||||
|
|
||||||
class _PolicyPageState extends State<PolicyPage> {
|
class _PolicyPageState extends State<PolicyPage> {
|
||||||
late Future<String> _termsLoader;
|
late Future<String> _termsLoader;
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
|
|
||||||
static const termsPath = 'assets/terms.md';
|
static const termsPath = 'assets/terms.md';
|
||||||
static const termsDirection = TextDirection.ltr;
|
static const termsDirection = TextDirection.ltr;
|
||||||
|
@ -39,11 +37,8 @@ class _PolicyPageState extends State<PolicyPage> {
|
||||||
child: FocusableActionDetector(
|
child: FocusableActionDetector(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
shortcuts: const {
|
shortcuts: const {
|
||||||
SingleActivator(LogicalKeyboardKey.arrowUp): VerticalScrollIntent.up(),
|
SingleActivator(LogicalKeyboardKey.arrowUp): ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
||||||
SingleActivator(LogicalKeyboardKey.arrowDown): VerticalScrollIntent.down(),
|
SingleActivator(LogicalKeyboardKey.arrowDown): ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
VerticalScrollIntent: VerticalScrollIntentAction(scrollController: _scrollController),
|
|
||||||
},
|
},
|
||||||
child: Center(
|
child: Center(
|
||||||
child: FutureBuilder<String>(
|
child: FutureBuilder<String>(
|
||||||
|
@ -54,7 +49,7 @@ class _PolicyPageState extends State<PolicyPage> {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: MarkdownContainer(
|
child: MarkdownContainer(
|
||||||
scrollController: _scrollController,
|
scrollController: PrimaryScrollController.of(context),
|
||||||
data: terms,
|
data: terms,
|
||||||
textDirection: termsDirection,
|
textDirection: termsDirection,
|
||||||
),
|
),
|
||||||
|
|
|
@ -49,6 +49,7 @@ class AboutTranslators extends StatelessWidget {
|
||||||
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
|
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
|
||||||
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
|
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
|
||||||
Contributor('kaajjo', 'claymanoff@gmail.com'),
|
Contributor('kaajjo', 'claymanoff@gmail.com'),
|
||||||
|
Contributor('Eduardo Malaspina', 'vaio0@swismail.com'),
|
||||||
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
|
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
|
||||||
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
|
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
|
||||||
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
|
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
|
||||||
|
|
|
@ -33,6 +33,7 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/route_tracker.dart';
|
import 'package:aves/widgets/common/behaviour/route_tracker.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/durations_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/home_page.dart';
|
import 'package:aves/widgets/home_page.dart';
|
||||||
|
@ -190,18 +191,16 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// place the settings provider above `MaterialApp`
|
// place the settings provider above `MaterialApp`
|
||||||
// so it can be used during navigation transitions
|
// so it can be used during navigation transitions
|
||||||
return Provider<AppFlavor>.value(
|
return MultiProvider(
|
||||||
value: widget.flavor,
|
providers: [
|
||||||
child: ChangeNotifierProvider<Settings>.value(
|
Provider<AppFlavor>.value(value: widget.flavor),
|
||||||
value: settings,
|
ChangeNotifierProvider<Settings>.value(value: settings),
|
||||||
child: ListenableProvider<ValueNotifier<AppMode>>.value(
|
ListenableProvider<ValueNotifier<AppMode>>.value(value: _appModeNotifier),
|
||||||
value: _appModeNotifier,
|
Provider<CollectionSource>.value(value: _mediaStoreSource),
|
||||||
child: Provider<CollectionSource>.value(
|
Provider<TvRailController>.value(value: _tvRailController),
|
||||||
value: _mediaStoreSource,
|
DurationsProvider(),
|
||||||
child: Provider<TvRailController>.value(
|
HighlightInfoProvider(),
|
||||||
value: _tvRailController,
|
],
|
||||||
child: DurationsProvider(
|
|
||||||
child: HighlightInfoProvider(
|
|
||||||
child: OverlaySupport(
|
child: OverlaySupport(
|
||||||
child: FutureBuilder<void>(
|
child: FutureBuilder<void>(
|
||||||
future: _appSetup,
|
future: _appSetup,
|
||||||
|
@ -245,9 +244,21 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
|
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
|
||||||
return Shortcuts(
|
return Shortcuts(
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
// handle Android TV remote `select` button
|
// handle Android TV remote `select` button (KEYCODE_DPAD_CENTER)
|
||||||
|
// the following keys are already handled by default:
|
||||||
|
// KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER
|
||||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||||
},
|
},
|
||||||
|
child: MediaQuery.fromWindow(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
// disable accessible navigation, as it impacts snack bar action timer
|
||||||
|
// for all users of apps registered as accessibility services,
|
||||||
|
// even though they are not for accessibility purposes (like TalkBack is)
|
||||||
|
accessibleNavigation: false,
|
||||||
|
),
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
navigatorKey: AvesApp.navigatorKey,
|
navigatorKey: AvesApp.navigatorKey,
|
||||||
home: home,
|
home: home,
|
||||||
|
@ -266,6 +277,11 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
supportedLocales: AvesApp.supportedLocales,
|
supportedLocales: AvesApp.supportedLocales,
|
||||||
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
|
||||||
scrollBehavior: StretchMaterialScrollBehavior(),
|
scrollBehavior: StretchMaterialScrollBehavior(),
|
||||||
|
useInheritedMediaQuery: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -275,12 +291,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +364,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
debugPrint('$runtimeType lifecycle ${state.name}');
|
|
||||||
reportService.log('Lifecycle ${state.name}');
|
reportService.log('Lifecycle ${state.name}');
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
|
@ -547,6 +556,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
'locales': WidgetsBinding.instance.window.locales.join(', '),
|
'locales': WidgetsBinding.instance.window.locales.join(', '),
|
||||||
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
||||||
});
|
});
|
||||||
|
await reportService.log('Launch');
|
||||||
setState(() => _navigatorObservers = [
|
setState(() => _navigatorObservers = [
|
||||||
AvesApp.pageRouteObserver,
|
AvesApp.pageRouteObserver,
|
||||||
ReportingRouteTracker(),
|
ReportingRouteTracker(),
|
||||||
|
@ -557,7 +567,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
debugPrint('$runtimeType onNewIntent with intentData=$intentData');
|
debugPrint('$runtimeType onNewIntent with intentData=$intentData');
|
||||||
|
|
||||||
// do not reset when relaunching the app
|
// do not reset when relaunching the app
|
||||||
if (_appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return;
|
if (_appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) {
|
||||||
|
reportService.log('Relaunch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
reportService.log('New intent data=$intentData');
|
reportService.log('New intent data=$intentData');
|
||||||
AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
|
AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
|
||||||
|
|
|
@ -23,7 +23,9 @@ import 'package:aves/widgets/common/action_controls/togglers/favourite.dart';
|
||||||
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
|
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/container.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/popup/expansion_panel.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
|
@ -405,10 +407,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
|
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
|
||||||
PopupMenuItem<EntrySetAction>(
|
PopupMenuExpansionPanel<EntrySetAction>(
|
||||||
enabled: hasSelection,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
child: PopupMenuItemExpansionPanel<EntrySetAction>(
|
|
||||||
enabled: hasSelection,
|
enabled: hasSelection,
|
||||||
value: 'edit',
|
value: 'edit',
|
||||||
icon: AIcons.edit,
|
icon: AIcons.edit,
|
||||||
|
@ -418,7 +417,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
|
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -529,7 +527,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuItem<EntrySetAction> _buildRotateAndFlipMenuItems(
|
PopupMenuEntry<EntrySetAction> _buildRotateAndFlipMenuItems(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required bool Function(EntrySetAction action) canApply,
|
required bool Function(EntrySetAction action) canApply,
|
||||||
}) {
|
}) {
|
||||||
|
@ -558,11 +556,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return PopupMenuItem(
|
return PopupMenuItemContainer(
|
||||||
child: TooltipTheme(
|
|
||||||
data: TooltipTheme.of(context).copyWith(
|
|
||||||
preferBelow: false,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
buildDivider(),
|
buildDivider(),
|
||||||
|
@ -574,7 +568,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
buildDivider(),
|
buildDivider(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
@ -19,8 +20,9 @@ import 'package:aves/widgets/collection/draggable_thumb_label.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
|
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
|
||||||
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
||||||
import 'package:aves/widgets/collection/grid/tile.dart';
|
import 'package:aves/widgets/collection/grid/tile.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
|
@ -39,8 +41,11 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||||
|
import 'package:aves/widgets/common/thumbnail/notifications.dart';
|
||||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||||||
|
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
@ -152,7 +157,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
||||||
tileAnimationDelay = Duration.zero;
|
tileAnimationDelay = Duration.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
return StreamBuilder(
|
return NotificationListener<OpenViewerNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
_goToViewer(collection, notification.entry);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: StreamBuilder(
|
||||||
stream: source.eventBus.on<AspectRatioChangedEvent>(),
|
stream: source.eventBus.on<AspectRatioChangedEvent>(),
|
||||||
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
|
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
|
@ -205,6 +215,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
||||||
tileAnimationDelay: tileAnimationDelay,
|
tileAnimationDelay: tileAnimationDelay,
|
||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -227,6 +238,36 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _goToViewer(CollectionLens collection, AvesEntry entry) {
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
Navigator.maybeOf(context)?.push(
|
||||||
|
TransparentMaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: EntryViewerPage.routeName),
|
||||||
|
pageBuilder: (context, a, sa) {
|
||||||
|
final viewerCollection = collection.copyWith(
|
||||||
|
listenToSource: false,
|
||||||
|
);
|
||||||
|
Widget child = EntryViewerPage(
|
||||||
|
collection: viewerCollection,
|
||||||
|
initialEntry: entry,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selection.isSelecting) {
|
||||||
|
child = MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickMediaInternal)),
|
||||||
|
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollectionSectionedContent extends StatefulWidget {
|
class _CollectionSectionedContent extends StatefulWidget {
|
||||||
|
@ -460,6 +501,8 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
||||||
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
|
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
|
||||||
selector: (context, layout) => layout.sectionLayouts,
|
selector: (context, layout) => layout.sectionLayouts,
|
||||||
builder: (context, sectionLayouts, child) {
|
builder: (context, sectionLayouts, child) {
|
||||||
|
final scrollController = widget.scrollController;
|
||||||
|
final offsetIncrementSnapThreshold = context.select<TileExtentController, double>((v) => (v.extentNotifier.value + v.spacing) / 4);
|
||||||
return DraggableScrollbar(
|
return DraggableScrollbar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
|
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
|
||||||
|
@ -467,7 +510,23 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
||||||
height: avesScrollThumbHeight,
|
height: avesScrollThumbHeight,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
controller: widget.scrollController,
|
controller: scrollController,
|
||||||
|
dragOffsetSnapper: (scrollOffset, offsetIncrement) {
|
||||||
|
if (offsetIncrement > offsetIncrementSnapThreshold && scrollOffset < scrollController.position.maxScrollExtent) {
|
||||||
|
final section = sectionLayouts.firstWhereOrNull((section) => section.hasChildAtOffset(scrollOffset));
|
||||||
|
if (section != null) {
|
||||||
|
if (section.maxOffset - section.minOffset < scrollController.position.viewportDimension) {
|
||||||
|
// snap to section header
|
||||||
|
return section.minOffset;
|
||||||
|
} else {
|
||||||
|
// snap to content row
|
||||||
|
final index = section.getMinChildIndexForScrollOffset(scrollOffset);
|
||||||
|
return section.indexToLayoutOffset(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scrollOffset;
|
||||||
|
},
|
||||||
crumbsBuilder: () => _getCrumbs(sectionLayouts),
|
crumbsBuilder: () => _getCrumbs(sectionLayouts),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
// padding to keep scroll thumb between app bar above and nav bar below
|
// padding to keep scroll thumb between app bar above and nav bar below
|
||||||
|
|
|
@ -14,7 +14,7 @@ import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/intent_service.dart';
|
import 'package:aves/services/intent_service.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
|
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
|
||||||
|
@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
|
||||||
class _CollectionPageState extends State<CollectionPage> {
|
class _CollectionPageState extends State<CollectionPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
late CollectionLens _collection;
|
late CollectionLens _collection;
|
||||||
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
||||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -146,7 +146,7 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||||
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
||||||
|
|
||||||
return NotificationListener<DraggableScrollBarNotification>(
|
return NotificationListener<DraggableScrollbarNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
_draggableScrollBarEventStreamController.add(notification.event);
|
_draggableScrollBarEventStreamController.add(notification.event);
|
||||||
return false;
|
return false;
|
||||||
|
@ -222,6 +222,7 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
|
final delayDuration = context.read<DurationsData>().staggeredAnimationPageTarget;
|
||||||
await Future.delayed(delayDuration + Durations.highlightScrollInitDelay);
|
await Future.delayed(delayDuration + Durations.highlightScrollInitDelay);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
final animate = context.read<Settings>().accessibilityAnimations.animate;
|
final animate = context.read<Settings>().accessibilityAnimations.animate;
|
||||||
context.read<HighlightInfo>().trackItem(item, animate: animate, highlightItem: item);
|
context.read<HighlightInfo>().trackItem(item, animate: animate, highlightItem: item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
|
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
|
@ -50,46 +51,57 @@ class EntryListDetails extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildRow(List<InlineSpan> spans, TextStyle style) {
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: spans,
|
||||||
|
),
|
||||||
|
style: style,
|
||||||
|
strutStyle: Constants.overflowStrutStyle,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetSpan _buildIconSpan(IconData icon, {EdgeInsetsDirectional padding = EdgeInsetsDirectional.zero}) {
|
||||||
|
return WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 8, bottom: 1) + padding,
|
||||||
|
child: Icon(icon),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDateRow(BuildContext context, TextStyle style) {
|
Widget _buildDateRow(BuildContext context, TextStyle style) {
|
||||||
final locale = context.l10n.localeName;
|
final locale = context.l10n.localeName;
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
|
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
|
||||||
|
|
||||||
return Row(
|
final size = entry.sizeBytes;
|
||||||
children: [
|
final sizeText = size != null ? formatFileSize(locale, size) : Constants.overlayUnknown;
|
||||||
const Icon(AIcons.date),
|
|
||||||
const SizedBox(width: 8),
|
return _buildRow(
|
||||||
Expanded(
|
[
|
||||||
child: Text(
|
_buildIconSpan(AIcons.date),
|
||||||
dateText,
|
TextSpan(text: dateText),
|
||||||
style: style,
|
_buildIconSpan(AIcons.size, padding: const EdgeInsetsDirectional.only(start: 8)),
|
||||||
strutStyle: Constants.overflowStrutStyle,
|
TextSpan(text: sizeText),
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
style,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLocationRow(BuildContext context, TextStyle style) {
|
Widget _buildLocationRow(BuildContext context, TextStyle style) {
|
||||||
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
|
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
|
||||||
|
|
||||||
return Row(
|
return _buildRow(
|
||||||
children: [
|
[
|
||||||
const Icon(AIcons.location),
|
_buildIconSpan(AIcons.location),
|
||||||
const SizedBox(width: 8),
|
TextSpan(text: location),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
location,
|
|
||||||
style: style,
|
|
||||||
strutStyle: Constants.overflowStrutStyle,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
style,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,9 @@ import 'package:aves/model/source/enums/enums.dart';
|
||||||
import 'package:aves/services/intent_service.dart';
|
import 'package:aves/services/intent_service.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_details.dart';
|
import 'package:aves/widgets/collection/grid/list_details.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
|
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
|
||||||
import 'package:aves/widgets/common/grid/scaling.dart';
|
import 'package:aves/widgets/common/grid/scaling.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
import 'package:aves/widgets/common/thumbnail/notifications.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ class InteractiveTile extends StatelessWidget {
|
||||||
if (selection.isSelecting) {
|
if (selection.isSelecting) {
|
||||||
selection.toggleSelection(entry);
|
selection.toggleSelection(entry);
|
||||||
} else {
|
} else {
|
||||||
_goToViewer(context);
|
OpenViewerNotification(entry).dispatch(context);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AppMode.pickSingleMediaExternal:
|
case AppMode.pickSingleMediaExternal:
|
||||||
|
@ -79,24 +78,6 @@ class InteractiveTile extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _goToViewer(BuildContext context) {
|
|
||||||
Navigator.maybeOf(context)?.push(
|
|
||||||
TransparentMaterialPageRoute(
|
|
||||||
settings: const RouteSettings(name: EntryViewerPage.routeName),
|
|
||||||
pageBuilder: (context, a, sa) {
|
|
||||||
final viewerCollection = collection.copyWith(
|
|
||||||
listenToSource: false,
|
|
||||||
);
|
|
||||||
assert(viewerCollection.sortedEntries.map((entry) => entry.id).contains(entry.id));
|
|
||||||
return EntryViewerPage(
|
|
||||||
collection: viewerCollection,
|
|
||||||
initialEntry: entry,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tile extends StatelessWidget {
|
class Tile extends StatelessWidget {
|
||||||
|
|
|
@ -56,6 +56,19 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final _hasChooser = hasChooser;
|
final _hasChooser = hasChooser;
|
||||||
|
|
||||||
|
Widget child = IconButton(
|
||||||
|
icon: icon,
|
||||||
|
onPressed: widget.onPressed,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
tooltip: _hasChooser ? null : tooltip,
|
||||||
|
);
|
||||||
|
if (_hasChooser) {
|
||||||
|
child = Semantics(
|
||||||
|
tooltip: tooltip,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onLongPressStart: _hasChooser ? _showChooser : null,
|
onLongPressStart: _hasChooser ? _showChooser : null,
|
||||||
|
@ -70,12 +83,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
onLongPressCancel: _clearChooserOverlayEntry,
|
onLongPressCancel: _clearChooserOverlayEntry,
|
||||||
child: IconButton(
|
child: child,
|
||||||
icon: icon,
|
|
||||||
onPressed: widget.onPressed,
|
|
||||||
focusNode: widget.focusNode,
|
|
||||||
tooltip: _hasChooser ? null : tooltip,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/actions/share_actions.dart';
|
import 'package:aves/model/actions/share_actions.dart';
|
||||||
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
|
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ShareQuickChooser extends StatelessWidget {
|
class ShareQuickChooser extends StatelessWidget {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:aves/model/query.dart';
|
import 'package:aves/model/query.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -30,7 +30,7 @@ import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
|
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
|
||||||
import 'package:aves/widgets/viewer/notifications.dart';
|
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -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 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/scroll_label.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/transition.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
adapted from package `draggable_scrollbar` v0.0.4:
|
adapted from package `draggable_scrollbar` v0.0.4:
|
||||||
|
@ -57,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
/// The ScrollController for the BoxScrollView
|
/// The ScrollController for the BoxScrollView
|
||||||
final ScrollController controller;
|
final ScrollController controller;
|
||||||
|
|
||||||
|
final double Function(double scrollOffset, double offsetIncrement)? dragOffsetSnapper;
|
||||||
|
|
||||||
/// The view that will be scrolled with the scroll thumb
|
/// The view that will be scrolled with the scroll thumb
|
||||||
final ScrollView child;
|
final ScrollView child;
|
||||||
|
|
||||||
|
@ -66,6 +70,7 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
required this.scrollThumbSize,
|
required this.scrollThumbSize,
|
||||||
required this.scrollThumbBuilder,
|
required this.scrollThumbBuilder,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
this.dragOffsetSnapper,
|
||||||
this.crumbsBuilder,
|
this.crumbsBuilder,
|
||||||
this.padding = EdgeInsets.zero,
|
this.padding = EdgeInsets.zero,
|
||||||
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
|
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
|
||||||
|
@ -109,38 +114,10 @@ class DraggableScrollbar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScrollLabel extends StatelessWidget {
|
|
||||||
final Animation<double> animation;
|
|
||||||
final Color backgroundColor;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const ScrollLabel({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.animation,
|
|
||||||
required this.backgroundColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsetsDirectional.only(end: 12.0),
|
|
||||||
child: Material(
|
|
||||||
elevation: 4.0,
|
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
|
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
|
||||||
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
|
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
|
||||||
bool _isDragInProcess = false;
|
bool _isDragInProcess = false;
|
||||||
|
double _boundlessThumbOffset = 0, _offsetIncrement = 0;
|
||||||
late Offset _longPressLastGlobalPosition;
|
late Offset _longPressLastGlobalPosition;
|
||||||
|
|
||||||
late AnimationController _thumbAnimationController;
|
late AnimationController _thumbAnimationController;
|
||||||
|
@ -239,7 +216,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
RepaintBoundary(
|
// exclude semantics, otherwise this layer will block access to content layers below when using TalkBack
|
||||||
|
ExcludeSemantics(
|
||||||
|
child: RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onLongPressStart: (details) {
|
onLongPressStart: (details) {
|
||||||
_longPressLastGlobalPosition = details.globalPosition;
|
_longPressLastGlobalPosition = details.globalPosition;
|
||||||
|
@ -275,6 +254,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -304,7 +284,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onVerticalDragStart() {
|
void _onVerticalDragStart() {
|
||||||
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragStart).dispatch(context);
|
const DraggableScrollbarNotification(DraggableScrollbarEvent.dragStart).dispatch(context);
|
||||||
|
_boundlessThumbOffset = _thumbOffsetNotifier.value;
|
||||||
|
_offsetIncrement = 1 / thumbMaxScrollExtent * controller.position.maxScrollExtent;
|
||||||
_labelAnimationController.forward();
|
_labelAnimationController.forward();
|
||||||
_fadeoutTimer?.cancel();
|
_fadeoutTimer?.cancel();
|
||||||
_showThumb();
|
_showThumb();
|
||||||
|
@ -316,17 +298,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
||||||
_showThumb();
|
_showThumb();
|
||||||
if (_isDragInProcess) {
|
if (_isDragInProcess) {
|
||||||
// thumb offset
|
// thumb offset
|
||||||
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
|
_boundlessThumbOffset += deltaY;
|
||||||
|
_thumbOffsetNotifier.value = _boundlessThumbOffset.clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
|
||||||
|
|
||||||
// scroll offset
|
// scroll offset
|
||||||
final min = controller.position.minScrollExtent;
|
final min = controller.position.minScrollExtent;
|
||||||
final max = controller.position.maxScrollExtent;
|
final max = controller.position.maxScrollExtent;
|
||||||
controller.jumpTo((_thumbOffsetNotifier.value / thumbMaxScrollExtent * max).clamp(min, max));
|
final scrollOffset = _thumbOffsetNotifier.value / thumbMaxScrollExtent * max;
|
||||||
|
controller.jumpTo((widget.dragOffsetSnapper?.call(scrollOffset, _offsetIncrement) ?? scrollOffset).clamp(min, max));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onVerticalDragEnd() {
|
void _onVerticalDragEnd() {
|
||||||
const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context);
|
const DraggableScrollbarNotification(DraggableScrollbarEvent.dragEnd).dispatch(context);
|
||||||
_scheduleFadeout();
|
_scheduleFadeout();
|
||||||
setState(() => _isDragInProcess = false);
|
setState(() => _isDragInProcess = false);
|
||||||
}
|
}
|
||||||
|
@ -373,79 +357,3 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///This cut 2 lines in arrow shape
|
|
||||||
class ArrowClipper extends CustomClipper<Path> {
|
|
||||||
@override
|
|
||||||
Path getClip(Size size) {
|
|
||||||
final path = Path();
|
|
||||||
path.lineTo(0.0, size.height);
|
|
||||||
path.lineTo(size.width, size.height);
|
|
||||||
path.lineTo(size.width, 0.0);
|
|
||||||
path.lineTo(0.0, 0.0);
|
|
||||||
path.close();
|
|
||||||
|
|
||||||
const arrowWidth = 8.0;
|
|
||||||
final startPointX = (size.width - arrowWidth) / 2;
|
|
||||||
var startPointY = size.height / 2 - arrowWidth / 2;
|
|
||||||
path.moveTo(startPointX, startPointY);
|
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
|
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY);
|
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
|
||||||
path.lineTo(startPointX, startPointY + 1.0);
|
|
||||||
path.close();
|
|
||||||
|
|
||||||
startPointY = size.height / 2 + arrowWidth / 2;
|
|
||||||
path.moveTo(startPointX + arrowWidth, startPointY);
|
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
|
|
||||||
path.lineTo(startPointX, startPointY);
|
|
||||||
path.lineTo(startPointX, startPointY - 1.0);
|
|
||||||
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
|
||||||
path.close();
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SlideFadeTransition extends StatelessWidget {
|
|
||||||
final Animation<double> animation;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const SlideFadeTransition({
|
|
||||||
super.key,
|
|
||||||
required this.animation,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: animation,
|
|
||||||
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: Tween(
|
|
||||||
begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
|
|
||||||
end: Offset.zero,
|
|
||||||
).animate(animation),
|
|
||||||
child: FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class DraggableScrollBarNotification extends Notification {
|
|
||||||
final DraggableScrollBarEvent event;
|
|
||||||
|
|
||||||
const DraggableScrollBarNotification(this.event);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DraggableScrollBarEvent { dragStart, dragEnd }
|
|
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/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MenuRow extends StatelessWidget {
|
class PopupMenuExpansionPanel<T> extends PopupMenuEntry<T> {
|
||||||
final String text;
|
|
||||||
final Widget? icon;
|
|
||||||
|
|
||||||
const MenuRow({
|
|
||||||
super.key,
|
|
||||||
required this.text,
|
|
||||||
this.icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (icon != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
|
||||||
child: IconTheme.merge(
|
|
||||||
data: IconThemeData(
|
|
||||||
color: ListTileTheme.of(context).iconColor,
|
|
||||||
),
|
|
||||||
child: icon!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(text),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// scale icons according to text scale
|
|
||||||
class MenuIconTheme extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const MenuIconTheme({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final iconTheme = IconTheme.of(context);
|
|
||||||
return IconTheme(
|
|
||||||
data: iconTheme.copyWith(
|
|
||||||
size: iconTheme.size! * MediaQuery.textScaleFactorOf(context),
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final String value;
|
final String value;
|
||||||
final ValueNotifier<String?> expandedNotifier;
|
final ValueNotifier<String?> expandedNotifier;
|
||||||
|
@ -64,9 +11,10 @@ class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<PopupMenuEntry<T>> items;
|
final List<PopupMenuEntry<T>> items;
|
||||||
|
|
||||||
PopupMenuItemExpansionPanel({
|
PopupMenuExpansionPanel({
|
||||||
super.key,
|
super.key,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.height = kMinInteractiveDimension,
|
||||||
required this.value,
|
required this.value,
|
||||||
ValueNotifier<String?>? expandedNotifier,
|
ValueNotifier<String?>? expandedNotifier,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
|
@ -75,10 +23,16 @@ class PopupMenuItemExpansionPanel<T> extends StatefulWidget {
|
||||||
}) : expandedNotifier = expandedNotifier ?? ValueNotifier(null);
|
}) : expandedNotifier = expandedNotifier ?? ValueNotifier(null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PopupMenuItemExpansionPanel<T>> createState() => _PopupMenuItemExpansionPanelState<T>();
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool represents(void value) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PopupMenuExpansionPanel<T>> createState() => _PopupMenuExpansionPanelState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionPanel<T>> {
|
class _PopupMenuExpansionPanelState<T> extends State<PopupMenuExpansionPanel<T>> {
|
||||||
// ref `_kMenuHorizontalPadding` used in `PopupMenuItem`
|
// ref `_kMenuHorizontalPadding` used in `PopupMenuItem`
|
||||||
static const double _horizontalPadding = 16;
|
static const double _horizontalPadding = 16;
|
||||||
|
|
||||||
|
@ -103,8 +57,13 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
children: [
|
children: [
|
||||||
ExpansionPanel(
|
ExpansionPanel(
|
||||||
headerBuilder: (context, isExpanded) => DefaultTextStyle(
|
headerBuilder: (context, isExpanded) {
|
||||||
|
return DefaultTextStyle(
|
||||||
style: style,
|
style: style,
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: widget.enabled ? null : Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
|
padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
|
||||||
child: MenuRow(
|
child: MenuRow(
|
||||||
|
@ -113,6 +72,8 @@ class _PopupMenuItemExpansionPanelState<T> extends State<PopupMenuItemExpansionP
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
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) {
|
children: _diffs.map((diff) {
|
||||||
final oldText = diff.item1;
|
final oldText = diff.item1;
|
||||||
final newText = diff.item2;
|
final newText = diff.item2;
|
||||||
final oldWidth = diff.item3;
|
final oldSize = diff.item3;
|
||||||
final newWidth = diff.item4;
|
final newSize = diff.item4;
|
||||||
final text = (_animation.value == 0 ? oldText : newText) ?? '';
|
final text = (_animation.value == 0 ? oldText : newText) ?? '';
|
||||||
return WidgetSpan(
|
return WidgetSpan(
|
||||||
child: AnimatedSize(
|
child: AnimatedSize(
|
||||||
|
@ -91,9 +91,10 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
children: [
|
children: [
|
||||||
...previousChildren.map(
|
...previousChildren.map(
|
||||||
(child) => ConstrainedBox(
|
(child) => ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints.tight(Size(
|
||||||
maxWidth: min(oldWidth, newWidth),
|
min(oldSize.width, newSize.width),
|
||||||
),
|
min(oldSize.height, newSize.height),
|
||||||
|
)),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -116,14 +117,16 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double textWidth(String text) {
|
Size textSize(String text) {
|
||||||
final para = RenderParagraph(
|
final para = RenderParagraph(
|
||||||
TextSpan(text: text, style: widget.textStyle),
|
TextSpan(text: text, style: widget.textStyle),
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
textScaleFactor: MediaQuery.textScaleFactorOf(context),
|
textScaleFactor: MediaQuery.textScaleFactorOf(context),
|
||||||
strutStyle: widget.strutStyle,
|
strutStyle: widget.strutStyle,
|
||||||
)..layout(const BoxConstraints(), parentUsesSize: true);
|
)..layout(const BoxConstraints(), parentUsesSize: true);
|
||||||
return para.getMaxIntrinsicWidth(double.infinity);
|
final width = para.getMaxIntrinsicWidth(double.infinity);
|
||||||
|
final height = para.getMaxIntrinsicHeight(double.infinity);
|
||||||
|
return Size(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// use an adaptation of Google's `Diff Match and Patch`
|
// use an adaptation of Google's `Diff Match and Patch`
|
||||||
|
@ -140,15 +143,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(d.map((diff) {
|
..addAll(d.map((diff) {
|
||||||
final text = diff.text;
|
final text = diff.text;
|
||||||
|
final size = textSize(text);
|
||||||
switch (diff.operation) {
|
switch (diff.operation) {
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
return Tuple4(text, null, textWidth(text), .0);
|
return Tuple4(text, null, size, Size.zero);
|
||||||
case Operation.insert:
|
case Operation.insert:
|
||||||
return Tuple4(null, text, .0, textWidth(text));
|
return Tuple4(null, text, Size.zero, size);
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
default:
|
default:
|
||||||
final width = textWidth(text);
|
return Tuple4(text, text, size, size);
|
||||||
return Tuple4(text, text, width, width);
|
|
||||||
}
|
}
|
||||||
}).fold<List<_TextDiff>>([], (prev, v) {
|
}).fold<List<_TextDiff>>([], (prev, v) {
|
||||||
if (prev.isNotEmpty) {
|
if (prev.isNotEmpty) {
|
||||||
|
@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
return [...prev, v];
|
return [...prev, v];
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// void _computeDiff(String oldText, String newText) {
|
|
||||||
// final oldCharacters = oldText.characters.toList();
|
|
||||||
// final newCharacters = newText.characters.toList();
|
|
||||||
// final diffResult = calculateListDiff<String>(oldCharacters, newCharacters, detectMoves: false);
|
|
||||||
// final updates = diffResult.getUpdatesWithData().toList();
|
|
||||||
// List<TextDiff> diffs = [];
|
|
||||||
// DataDiffUpdate<String>? pendingUpdate;
|
|
||||||
// int lastPos = oldCharacters.length;
|
|
||||||
// void addKeep(int pos) {
|
|
||||||
// if (pos < lastPos) {
|
|
||||||
// final text = oldCharacters.sublist(pos, lastPos).join();
|
|
||||||
// final width = textWidth(text);
|
|
||||||
// diffs.insert(0, Tuple4(text, text, width, width));
|
|
||||||
// lastPos = pos;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// void commit(DataDiffUpdate<String>? update) {
|
|
||||||
// update?.when(
|
|
||||||
// insert: (pos, data) {
|
|
||||||
// addKeep(pos);
|
|
||||||
// diffs.insert(0, Tuple4(null, data, 0, textWidth(data)));
|
|
||||||
// lastPos = pos;
|
|
||||||
// },
|
|
||||||
// remove: (pos, data) {
|
|
||||||
// addKeep(pos + data.length);
|
|
||||||
// diffs.insert(0, Tuple4(data, null, textWidth(data), 0));
|
|
||||||
// lastPos = pos;
|
|
||||||
// },
|
|
||||||
// change: (pos, oldData, newData) {
|
|
||||||
// addKeep(pos + oldData.length);
|
|
||||||
// diffs.insert(0, Tuple4(oldData, newData, textWidth(oldData), textWidth(newData)));
|
|
||||||
// lastPos = pos;
|
|
||||||
// },
|
|
||||||
// move: (from, to, data) {
|
|
||||||
// assert(false, '`move` update: from=$from, to=$from, data=$data');
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for (var update in updates) {
|
|
||||||
// update.when(
|
|
||||||
// insert: (pos, data) {
|
|
||||||
// if (pendingUpdate == null) {
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (pendingUpdate is DataInsert) {
|
|
||||||
// final pendingInsert = pendingUpdate as DataInsert;
|
|
||||||
// if (pendingInsert.position == pos) {
|
|
||||||
// // merge insertions
|
|
||||||
// pendingUpdate = DataInsert(position: pos, data: data + pendingInsert.data);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// } else if (pendingUpdate is DataRemove) {
|
|
||||||
// final pendingRemove = pendingUpdate as DataRemove;
|
|
||||||
// if (pendingRemove.position == pos) {
|
|
||||||
// // convert to change
|
|
||||||
// pendingUpdate = DataChange(position: pos, oldData: pendingRemove.data, newData: data);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// } else if (pendingUpdate is DataChange) {
|
|
||||||
// final pendingChange = pendingUpdate as DataChange;
|
|
||||||
// if (pendingChange.position == pos) {
|
|
||||||
// // merge changes
|
|
||||||
// pendingUpdate = DataChange(position: pos, oldData: pendingChange.oldData, newData: data + pendingChange.newData);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// commit(pendingUpdate);
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// },
|
|
||||||
// remove: (pos, data) {
|
|
||||||
// if (pendingUpdate == null) {
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (pendingUpdate is DataRemove) {
|
|
||||||
// final pendingRemove = pendingUpdate as DataRemove;
|
|
||||||
// if (pendingRemove.position == pos + data.length) {
|
|
||||||
// // merge removals
|
|
||||||
// pendingUpdate = DataRemove(position: pos, data: data + pendingRemove.data);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// commit(pendingUpdate);
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// },
|
|
||||||
// change: (pos, oldData, newData) {
|
|
||||||
// assert(false, '`change` update: from=$pos, oldData=$oldData, newData=$newData');
|
|
||||||
// },
|
|
||||||
// move: (from, to, data) {
|
|
||||||
// assert(false, '`move` update: from=$from, to=$from, data=$data');
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// commit(pendingUpdate);
|
|
||||||
// addKeep(0);
|
|
||||||
// _diffs
|
|
||||||
// ..clear()
|
|
||||||
// ..addAll(diffs);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _TextDiff = Tuple4<String?, String?, double, double>;
|
typedef _TextDiff = Tuple4<String?, String?, Size, Size>;
|
||||||
|
|
|
@ -1,37 +1,22 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class VerticalScrollIntent extends Intent {
|
class ScrollControllerAction extends CallbackAction<ScrollIntent> {
|
||||||
const VerticalScrollIntent({
|
ScrollControllerAction({
|
||||||
required this.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const VerticalScrollIntent.up() : type = VerticalScrollDirection.up;
|
|
||||||
|
|
||||||
const VerticalScrollIntent.down() : type = VerticalScrollDirection.down;
|
|
||||||
|
|
||||||
final VerticalScrollDirection type;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum VerticalScrollDirection {
|
|
||||||
up,
|
|
||||||
down,
|
|
||||||
}
|
|
||||||
|
|
||||||
class VerticalScrollIntentAction extends CallbackAction<VerticalScrollIntent> {
|
|
||||||
VerticalScrollIntentAction({
|
|
||||||
required ScrollController scrollController,
|
required ScrollController scrollController,
|
||||||
}) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController));
|
}) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController));
|
||||||
|
|
||||||
static void _onScrollIntent(
|
static void _onScrollIntent(
|
||||||
VerticalScrollIntent intent,
|
ScrollIntent intent,
|
||||||
ScrollController scrollController,
|
ScrollController scrollController,
|
||||||
) {
|
) {
|
||||||
late int factor;
|
late int factor;
|
||||||
switch (intent.type) {
|
switch (intent.direction) {
|
||||||
case VerticalScrollDirection.up:
|
case AxisDirection.up:
|
||||||
|
case AxisDirection.left:
|
||||||
factor = -1;
|
factor = -1;
|
||||||
break;
|
break;
|
||||||
case VerticalScrollDirection.down:
|
case AxisDirection.down:
|
||||||
|
case AxisDirection.right:
|
||||||
factor = 1;
|
factor = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,28 +22,13 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting);
|
final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting);
|
||||||
final child = isSelecting
|
return AnimatedSwitcher(
|
||||||
? Selector<Selection<T>, bool>(
|
|
||||||
selector: (context, selection) => selection.isSelected([item]),
|
|
||||||
builder: (context, isSelected, child) {
|
|
||||||
var child = isSelecting
|
|
||||||
? OverlayIcon(
|
|
||||||
key: ValueKey(isSelected),
|
|
||||||
icon: isSelected ? AIcons.selected : AIcons.unselected,
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
)
|
|
||||||
: const SizedBox();
|
|
||||||
child = AnimatedSwitcher(
|
|
||||||
duration: duration,
|
duration: duration,
|
||||||
switchInCurve: Curves.easeOutBack,
|
child: isSelecting
|
||||||
switchOutCurve: Curves.easeOutBack,
|
? Selector<Selection<T>, bool>(
|
||||||
transitionBuilder: (child, animation) => ScaleTransition(
|
selector: (context, selection) => selection.isSelected({item}),
|
||||||
scale: animation,
|
builder: (context, isSelected, child) {
|
||||||
child: child,
|
return AnimatedContainer(
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
child = AnimatedContainer(
|
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -51,15 +36,24 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: duration,
|
||||||
|
switchInCurve: Curves.easeOutBack,
|
||||||
|
switchOutCurve: Curves.easeOutBack,
|
||||||
|
transitionBuilder: (child, animation) => ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
child: child,
|
child: child,
|
||||||
|
),
|
||||||
|
child: OverlayIcon(
|
||||||
|
key: ValueKey(isSelected),
|
||||||
|
icon: isSelected ? AIcons.selected : AIcons.unselected,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return child;
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: const SizedBox();
|
: const SizedBox(),
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: duration,
|
|
||||||
child: child,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,30 +206,34 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
|
||||||
void _toggleSelectionToIndex(int toIndex) {
|
void _toggleSelectionToIndex(int toIndex) {
|
||||||
if (toIndex == -1) return;
|
if (toIndex == -1) return;
|
||||||
|
|
||||||
|
Iterable<T> getRange(int start, int end) => items.getRange(start, end);
|
||||||
final selection = context.read<Selection<T>>();
|
final selection = context.read<Selection<T>>();
|
||||||
|
void addRange(int start, int end) => selection.addToSelection(getRange(start, end));
|
||||||
|
void removeRange(int start, int end) => selection.removeFromSelection(getRange(start, end));
|
||||||
|
|
||||||
if (_selecting) {
|
if (_selecting) {
|
||||||
if (toIndex <= _fromIndex) {
|
if (toIndex <= _fromIndex) {
|
||||||
if (toIndex < _lastToIndex) {
|
if (toIndex < _lastToIndex) {
|
||||||
selection.addToSelection(items.getRange(toIndex, min(_fromIndex, _lastToIndex)));
|
addRange(toIndex, min(_fromIndex, _lastToIndex));
|
||||||
if (_fromIndex < _lastToIndex) {
|
if (_fromIndex < _lastToIndex) {
|
||||||
selection.removeFromSelection(items.getRange(_fromIndex + 1, _lastToIndex + 1));
|
removeRange(_fromIndex + 1, _lastToIndex + 1);
|
||||||
}
|
}
|
||||||
} else if (_lastToIndex < toIndex) {
|
} else if (_lastToIndex < toIndex) {
|
||||||
selection.removeFromSelection(items.getRange(_lastToIndex, toIndex));
|
removeRange(_lastToIndex, toIndex);
|
||||||
}
|
}
|
||||||
} else if (_fromIndex < toIndex) {
|
} else if (_fromIndex < toIndex) {
|
||||||
if (_lastToIndex < toIndex) {
|
if (_lastToIndex < toIndex) {
|
||||||
selection.addToSelection(items.getRange(max(_fromIndex, _lastToIndex), toIndex + 1));
|
addRange(max(_fromIndex, _lastToIndex), toIndex + 1);
|
||||||
if (_lastToIndex < _fromIndex) {
|
if (_lastToIndex < _fromIndex) {
|
||||||
selection.removeFromSelection(items.getRange(_lastToIndex, _fromIndex));
|
removeRange(_lastToIndex, _fromIndex);
|
||||||
}
|
}
|
||||||
} else if (toIndex < _lastToIndex) {
|
} else if (toIndex < _lastToIndex) {
|
||||||
selection.removeFromSelection(items.getRange(toIndex + 1, _lastToIndex + 1));
|
removeRange(toIndex + 1, _lastToIndex + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastToIndex = toIndex;
|
_lastToIndex = toIndex;
|
||||||
} else {
|
} else {
|
||||||
selection.removeFromSelection(items.getRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1));
|
removeRange(min(_fromIndex, toIndex), max(_fromIndex, toIndex) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,12 @@ class GridTheme extends StatelessWidget {
|
||||||
final fontSize = (iconSize * .7).floorToDouble();
|
final fontSize = (iconSize * .7).floorToDouble();
|
||||||
iconSize *= mq.textScaleFactor;
|
iconSize *= mq.textScaleFactor;
|
||||||
final highlightBorderWidth = extent * .1;
|
final highlightBorderWidth = extent * .1;
|
||||||
|
final interactiveDimension = min(iconSize * 2, kMinInteractiveDimension);
|
||||||
return GridThemeData(
|
return GridThemeData(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
highlightBorderWidth: highlightBorderWidth,
|
highlightBorderWidth: highlightBorderWidth,
|
||||||
|
interactiveDimension: interactiveDimension,
|
||||||
showFavourite: settings.showThumbnailFavourite,
|
showFavourite: settings.showThumbnailFavourite,
|
||||||
locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none,
|
locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none,
|
||||||
tagIcon: settings.thumbnailTagIcon,
|
tagIcon: settings.thumbnailTagIcon,
|
||||||
|
@ -52,7 +54,7 @@ class GridTheme extends StatelessWidget {
|
||||||
typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesEntry entry);
|
typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesEntry entry);
|
||||||
|
|
||||||
class GridThemeData {
|
class GridThemeData {
|
||||||
final double iconSize, fontSize, highlightBorderWidth;
|
final double iconSize, fontSize, highlightBorderWidth, interactiveDimension;
|
||||||
final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
|
final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
|
||||||
final bool showLocated, showUnlocated, showTagged, showUntagged;
|
final bool showLocated, showUnlocated, showTagged, showUntagged;
|
||||||
late final GridThemeIconBuilder iconBuilder;
|
late final GridThemeIconBuilder iconBuilder;
|
||||||
|
@ -61,6 +63,7 @@ class GridThemeData {
|
||||||
required this.iconSize,
|
required this.iconSize,
|
||||||
required this.fontSize,
|
required this.fontSize,
|
||||||
required this.highlightBorderWidth,
|
required this.highlightBorderWidth,
|
||||||
|
required this.interactiveDimension,
|
||||||
required this.showFavourite,
|
required this.showFavourite,
|
||||||
required ThumbnailOverlayLocationIcon locationIcon,
|
required ThumbnailOverlayLocationIcon locationIcon,
|
||||||
required ThumbnailOverlayTagIcon tagIcon,
|
required ThumbnailOverlayTagIcon tagIcon,
|
||||||
|
|
|
@ -14,7 +14,7 @@ import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/filter_bar.dart';
|
import 'package:aves/widgets/collection/filter_bar.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/arrow_clipper.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const double avesScrollThumbHeight = 48;
|
const double avesScrollThumbHeight = 48;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
import 'package:aves/utils/debouncer.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/viewer/notifications.dart';
|
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case MapNavigationButton.none:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
||||||
|
|
|
@ -256,7 +256,10 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resetRotation() async {
|
Future<void> _resetRotation() async {
|
||||||
final rotationTween = Tween<double>(begin: _leafletMapController.rotation, end: 0);
|
final rotation = _leafletMapController.rotation;
|
||||||
|
// prevent multiple turns
|
||||||
|
final begin = (rotation.abs() % 360) * rotation.sign;
|
||||||
|
final rotationTween = Tween<double>(begin: begin, end: 0);
|
||||||
await _animateCamera((animation) => _leafletMapController.rotate(rotationTween.evaluate(animation)));
|
await _animateCamera((animation) => _leafletMapController.rotate(rotationTween.evaluate(animation)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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:aves/model/highlight.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class HighlightInfoProvider extends StatelessWidget {
|
class HighlightInfoProvider extends ChangeNotifierProvider<HighlightInfo> {
|
||||||
final Widget child;
|
HighlightInfoProvider({
|
||||||
|
|
||||||
const HighlightInfoProvider({
|
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
super.child,
|
||||||
});
|
}) : super(
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ChangeNotifierProvider<HighlightInfo>(
|
|
||||||
create: (context) => HighlightInfo(),
|
create: (context) => HighlightInfo(),
|
||||||
child: child,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
import 'package:aves/widgets/common/grid/overlay.dart';
|
import 'package:aves/widgets/common/grid/overlay.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||||
|
import 'package:aves/widgets/common/thumbnail/notifications.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/overlay.dart';
|
import 'package:aves/widgets/common/thumbnail/overlay.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -44,11 +45,15 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
child,
|
child,
|
||||||
ThumbnailEntryOverlay(entry: entry),
|
ThumbnailEntryOverlay(entry: entry),
|
||||||
if (selectable)
|
if (selectable) ...[
|
||||||
GridItemSelectionOverlay<AvesEntry>(
|
GridItemSelectionOverlay<AvesEntry>(
|
||||||
item: entry,
|
item: entry,
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
),
|
),
|
||||||
|
ThumbnailZoomOverlay(
|
||||||
|
onZoom: () => OpenViewerNotification(entry).dispatch(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
if (highlightable) ThumbnailHighlightOverlay(entry: entry),
|
if (highlightable) ThumbnailHighlightOverlay(entry: entry),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
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/entry.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
import 'package:aves/widgets/common/grid/theme.dart';
|
import 'package:aves/widgets/common/grid/theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -70,3 +73,43 @@ class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ThumbnailZoomOverlay extends StatelessWidget {
|
||||||
|
final VoidCallback? onZoom;
|
||||||
|
|
||||||
|
const ThumbnailZoomOverlay({
|
||||||
|
super.key,
|
||||||
|
this.onZoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const alignment = AlignmentDirectional.bottomEnd;
|
||||||
|
static const duration = Durations.thumbnailOverlayAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
|
||||||
|
final interactiveDimension = context.select<GridThemeData, double>((t) => t.interactiveDimension);
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: duration,
|
||||||
|
child: isSelecting
|
||||||
|
? Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onZoom,
|
||||||
|
child: Container(
|
||||||
|
alignment: alignment,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 2),
|
||||||
|
width: interactiveDimension,
|
||||||
|
height: interactiveDimension,
|
||||||
|
child: Icon(
|
||||||
|
AIcons.showFullscreen,
|
||||||
|
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -87,14 +87,14 @@ class TileExtentController {
|
||||||
int _effectiveColumnCountForExtent(double extent) {
|
int _effectiveColumnCountForExtent(double extent) {
|
||||||
if (extent > 0) {
|
if (extent > 0) {
|
||||||
final columnCount = _columnCountForExtent(extent);
|
final columnCount = _columnCountForExtent(extent);
|
||||||
final countMin = _effectiveColumnCountMin();
|
|
||||||
final countMax = _effectiveColumnCountMax();
|
final countMax = _effectiveColumnCountMax();
|
||||||
|
final countMin = min(_effectiveColumnCountMin(), countMax);
|
||||||
return columnCount.round().clamp(countMin, countMax);
|
return columnCount.round().clamp(countMin, countMax);
|
||||||
}
|
}
|
||||||
return columnCountDefault;
|
return columnCountDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
|
double get effectiveExtentMin => min(_extentForColumnCount(_effectiveColumnCountMax()), effectiveExtentMax);
|
||||||
|
|
||||||
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
|
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/analysis_service.dart';
|
import 'package:aves/services/analysis_service.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
|
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart';
|
import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/grid/theme.dart';
|
import 'package:aves/widgets/common/grid/theme.dart';
|
||||||
|
|
|
@ -10,7 +10,7 @@ import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/common/basic/color_list_tile.dart';
|
import 'package:aves/widgets/common/basic/list_tiles/color.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
|
|
|
@ -39,6 +39,7 @@ class _EditVaultDialogState extends State<EditVaultDialog> {
|
||||||
final List<VaultLockType> _lockTypeOptions = [
|
final List<VaultLockType> _lockTypeOptions = [
|
||||||
if (device.canAuthenticateUser) VaultLockType.system,
|
if (device.canAuthenticateUser) VaultLockType.system,
|
||||||
if (device.canUseCrypto) ...[
|
if (device.canUseCrypto) ...[
|
||||||
|
VaultLockType.pattern,
|
||||||
VaultLockType.pin,
|
VaultLockType.pin,
|
||||||
VaultLockType.password,
|
VaultLockType.password,
|
||||||
],
|
],
|
||||||
|
|
|
@ -41,7 +41,16 @@ class _PasswordDialogState extends State<PasswordDialog> {
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
onSubmitted: (password) {
|
onSubmitted: _submit,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit(String password) {
|
||||||
if (widget.needConfirmation) {
|
if (widget.needConfirmation) {
|
||||||
if (_confirming) {
|
if (_confirming) {
|
||||||
final match = _firstPassword == password;
|
final match = _firstPassword == password;
|
||||||
|
@ -65,12 +74,5 @@ class _PasswordDialogState extends State<PasswordDialog> {
|
||||||
} else {
|
} else {
|
||||||
Navigator.maybeOf(context)?.pop<String>(password);
|
Navigator.maybeOf(context)?.pop<String>(password);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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,
|
controller: _controller,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (v) {},
|
onChanged: (v) {},
|
||||||
onCompleted: (pin) {
|
onCompleted: _submit,
|
||||||
|
animationType: AnimationType.scale,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
autoFocus: true,
|
||||||
|
autoDismissKeyboard: !widget.needConfirmation || _confirming,
|
||||||
|
pinTheme: PinTheme(
|
||||||
|
activeColor: colorScheme.onBackground,
|
||||||
|
inactiveColor: colorScheme.onBackground,
|
||||||
|
selectedColor: colorScheme.secondary,
|
||||||
|
selectedFillColor: colorScheme.secondary,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
shape: PinCodeFieldShape.box,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit(String pin) {
|
||||||
if (widget.needConfirmation) {
|
if (widget.needConfirmation) {
|
||||||
if (_confirming) {
|
if (_confirming) {
|
||||||
final match = _firstPin == pin;
|
final match = _firstPin == pin;
|
||||||
|
@ -61,23 +81,5 @@ class _PinDialogState extends State<PinDialog> {
|
||||||
} else {
|
} else {
|
||||||
Navigator.maybeOf(context)?.pop<String>(pin);
|
Navigator.maybeOf(context)?.pop<String>(pin);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
animationType: AnimationType.scale,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
autoFocus: true,
|
|
||||||
autoDismissKeyboard: !widget.needConfirmation || _confirming,
|
|
||||||
pinTheme: PinTheme(
|
|
||||||
activeColor: colorScheme.onBackground,
|
|
||||||
inactiveColor: colorScheme.onBackground,
|
|
||||||
selectedColor: colorScheme.secondary,
|
|
||||||
selectedFillColor: colorScheme.secondary,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
shape: PinCodeFieldShape.box,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import 'package:aves/model/vaults/details.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
|
|
|
@ -2,8 +2,8 @@ import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
|
||||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
|
@ -11,7 +11,7 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
|
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
|
|
|
@ -12,7 +12,8 @@ import 'package:aves/model/source/enums/enums.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
|
import 'package:aves/widgets/common/behaviour/pop/double_back.dart';
|
||||||
|
@ -61,7 +62,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final QueryTest<T> applyQuery;
|
final QueryTest<T> applyQuery;
|
||||||
final Widget Function() emptyBuilder;
|
final Widget Function() emptyBuilder;
|
||||||
final HeroType heroType;
|
final HeroType heroType;
|
||||||
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
FilterGridPage({
|
FilterGridPage({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -145,7 +146,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||||
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
|
||||||
|
|
||||||
return NotificationListener<DraggableScrollBarNotification>(
|
return NotificationListener<DraggableScrollbarNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
_draggableScrollBarEventStreamController.add(notification.event);
|
_draggableScrollBarEventStreamController.add(notification.event);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -229,9 +229,12 @@ class _HomePageState extends State<HomePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case AppMode.setWallpaper:
|
||||||
|
// for video playback storage
|
||||||
|
await metadataDb.init();
|
||||||
|
break;
|
||||||
case AppMode.pickMediaInternal:
|
case AppMode.pickMediaInternal:
|
||||||
case AppMode.pickFilterInternal:
|
case AppMode.pickFilterInternal:
|
||||||
case AppMode.setWallpaper:
|
|
||||||
case AppMode.slideshow:
|
case AppMode.slideshow:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import 'package:aves/utils/debouncer.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -32,7 +32,7 @@ import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||||
import 'package:aves/widgets/map/map_info_row.dart';
|
import 'package:aves/widgets/map/map_info_row.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||||
import 'package:aves/widgets/viewer/notifications.dart';
|
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue