Merge branch 'develop' into main
4
.github/workflows/check.yml
vendored
|
@ -14,8 +14,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: subosito/flutter-action@v1
|
- uses: subosito/flutter-action@v1
|
||||||
with:
|
with:
|
||||||
channel: stable
|
channel: dev
|
||||||
flutter-version: '1.22.6'
|
flutter-version: '2.1.0-12.1.pre'
|
||||||
|
|
||||||
- name: Clone the repository.
|
- name: Clone the repository.
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
8
.github/workflows/release.yml
vendored
|
@ -16,8 +16,8 @@ jobs:
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v1
|
- uses: subosito/flutter-action@v1
|
||||||
with:
|
with:
|
||||||
channel: stable
|
channel: dev
|
||||||
flutter-version: '1.22.6'
|
flutter-version: '2.1.0-12.1.pre'
|
||||||
|
|
||||||
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
|
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
|
||||||
# https://issuetracker.google.com/issues/144111441
|
# https://issuetracker.google.com/issues/144111441
|
||||||
|
@ -50,8 +50,8 @@ jobs:
|
||||||
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
|
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
|
||||||
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
|
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
|
||||||
rm release.keystore.asc
|
rm release.keystore.asc
|
||||||
flutter build apk --bundle-sksl-path shaders_1.22.6.sksl.json
|
flutter build apk --bundle-sksl-path shaders_2.1.0-12.1.pre.sksl.json
|
||||||
flutter build appbundle --bundle-sksl-path shaders_1.22.6.sksl.json
|
flutter build appbundle --bundle-sksl-path shaders_2.1.0-12.1.pre.sksl.json
|
||||||
rm $AVES_STORE_FILE
|
rm $AVES_STORE_FILE
|
||||||
env:
|
env:
|
||||||
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
|
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
|
||||||
|
|
11
CHANGELOG.md
|
@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v1.3.6] - 2021-03-18
|
||||||
|
### Added
|
||||||
|
- Korean translation
|
||||||
|
- cover selection for albums / countries / tags
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgraded Flutter to dev v2.1.0-12.1.pre
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- various TIFF decoding fixes
|
||||||
|
|
||||||
## [v1.3.5] - 2021-02-26
|
## [v1.3.5] - 2021-02-26
|
||||||
### Added
|
### Added
|
||||||
- support Android KitKat, Lollipop & Marshmallow (API 19 ~ 23)
|
- support Android KitKat, Lollipop & Marshmallow (API 19 ~ 23)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
Aves is a gallery and metadata explorer app. It is built for Android, with Flutter.
|
Aves is a gallery and metadata explorer app. It is built for Android, with Flutter.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.2.1/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.2.1/S10/2-S10-image.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.2.1/S10/5-S10-stats.png" alt='Stats screenshot' height="400" />
|
<img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/2-S10-viewer.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/5-S10-stats.png" alt='Stats screenshot' height="400" />
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
|
@ -104,11 +104,11 @@ repositories {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||||
implementation 'androidx.core:core-ktx:1.5.0-beta01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
|
implementation 'androidx.core:core-ktx:1.5.0-beta03' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
||||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
|
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
|
||||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:f87db4305d' // forked, built by JitPack
|
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
|
|
||||||
kapt 'androidx.annotation:annotation:1.1.0'
|
kapt 'androidx.annotation:annotation:1.1.0'
|
||||||
|
|
4
android/app/src/debug/res/values-ko/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">아베스 [Debug]</string>
|
||||||
|
</resources>
|
|
@ -34,6 +34,7 @@ class MainActivity : FlutterActivity() {
|
||||||
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
|
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
|
||||||
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
|
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
|
||||||
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
|
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
|
||||||
|
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
|
||||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||||
MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(MetadataHandler(this))
|
MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(MetadataHandler(this))
|
||||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Geocoder
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
// as of 2021/03/10, geocoding packages exist but:
|
||||||
|
// - `geocoder` is unmaintained
|
||||||
|
// - `geocoding` method does not return `addressLine` (v2.0.0)
|
||||||
|
class GeocodingHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
private var geocoder: Geocoder? = null
|
||||||
|
|
||||||
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"getAddress" -> GlobalScope.launch(Dispatchers.IO) { Coresult.safe(call, result, ::getAddress) }
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAddress(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val latitude = call.argument<Number>("latitude")?.toDouble()
|
||||||
|
val longitude = call.argument<Number>("longitude")?.toDouble()
|
||||||
|
val localeString = call.argument<String>("locale")
|
||||||
|
val maxResults = call.argument<Int>("maxResults") ?: 1
|
||||||
|
if (latitude == null || longitude == null) {
|
||||||
|
result.error("getAddress-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Geocoder.isPresent()) {
|
||||||
|
result.error("getAddress-unavailable", "Geocoder is unavailable", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
geocoder = geocoder ?: if (localeString != null) {
|
||||||
|
val split = localeString.split("_")
|
||||||
|
val language = split[0]
|
||||||
|
val country = if (split.size > 1) split[1] else ""
|
||||||
|
Geocoder(context, Locale(language, country))
|
||||||
|
} else {
|
||||||
|
Geocoder(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val addresses = try {
|
||||||
|
geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.error("getAddress-exception", "failed to get address", e.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addresses.isEmpty()) {
|
||||||
|
result.error("getAddress-empty", "failed to find any address for latitude=$latitude, longitude=$longitude", null)
|
||||||
|
} else {
|
||||||
|
val addressMapList: ArrayList<Map<String, String?>> = ArrayList(addresses.map { address ->
|
||||||
|
hashMapOf(
|
||||||
|
"addressLine" to (0..address.maxAddressLineIndex).joinToString(", ") { i -> address.getAddressLine(i) },
|
||||||
|
"adminArea" to address.adminArea,
|
||||||
|
"countryCode" to address.countryCode,
|
||||||
|
"countryName" to address.countryName,
|
||||||
|
"featureName" to address.featureName,
|
||||||
|
"locality" to address.locality,
|
||||||
|
"postalCode" to address.postalCode,
|
||||||
|
"subAdminArea" to address.subAdminArea,
|
||||||
|
"subLocality" to address.subLocality,
|
||||||
|
"subThoroughfare" to address.subThoroughfare,
|
||||||
|
"thoroughfare" to address.thoroughfare,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
result.success(addressMapList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL = "deckers.thibault/aves/geocoding"
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import com.adobe.internal.xmp.properties.XMPPropertyInfo
|
||||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import com.drew.imaging.ImageMetadataReader
|
import com.drew.imaging.ImageMetadataReader
|
||||||
import com.drew.lang.Rational
|
import com.drew.lang.Rational
|
||||||
|
import com.drew.metadata.Tag
|
||||||
import com.drew.metadata.exif.*
|
import com.drew.metadata.exif.*
|
||||||
import com.drew.metadata.file.FileTypeDirectory
|
import com.drew.metadata.file.FileTypeDirectory
|
||||||
import com.drew.metadata.gif.GifAnimationDirectory
|
import com.drew.metadata.gif.GifAnimationDirectory
|
||||||
|
@ -47,7 +48,9 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
|
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
|
||||||
|
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
||||||
|
import deckers.thibault.aves.metadata.XMP.getSafeString
|
||||||
import deckers.thibault.aves.metadata.XMP.isPanorama
|
import deckers.thibault.aves.metadata.XMP.isPanorama
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.model.provider.FileImageProvider
|
import deckers.thibault.aves.model.provider.FileImageProvider
|
||||||
|
@ -123,17 +126,29 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
metadataMap[dirName] = dirMap
|
metadataMap[dirName] = dirMap
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
|
val tags = dir.tags
|
||||||
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
|
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
|
||||||
dirMap.putAll(dir.tags.map {
|
fun tagMapper(it: Tag): Pair<String, String> {
|
||||||
val name = if (it.hasTagName()) {
|
val name = if (it.hasTagName()) {
|
||||||
it.tagName
|
it.tagName
|
||||||
} else {
|
} else {
|
||||||
TiffTags.getTagName(it.tagType) ?: it.tagName
|
TiffTags.getTagName(it.tagType) ?: it.tagName
|
||||||
}
|
}
|
||||||
Pair(name, it.description)
|
return Pair(name, it.description)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (dir is ExifIFD0Directory && dir.isGeoTiff()) {
|
||||||
|
// split GeoTIFF tags in their own directory
|
||||||
|
val byGeoTiff = tags.groupBy { TiffTags.isGeoTiffTag(it.tagType) }
|
||||||
|
metadataMap["GeoTIFF"] = HashMap<String, String>().apply {
|
||||||
|
byGeoTiff[true]?.map { tagMapper(it) }?.let { putAll(it) }
|
||||||
|
}
|
||||||
|
byGeoTiff[false]?.map { tagMapper(it) }?.let { dirMap.putAll(it) }
|
||||||
|
} else {
|
||||||
|
dirMap.putAll(tags.map { tagMapper(it) })
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dirMap.putAll(dir.tags.map { Pair(it.tagName, it.description) })
|
dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
|
||||||
}
|
}
|
||||||
if (dir is XmpDirectory) {
|
if (dir is XmpDirectory) {
|
||||||
try {
|
try {
|
||||||
|
@ -593,10 +608,11 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
KEY_PAGE to i,
|
KEY_PAGE to i,
|
||||||
KEY_MIME_TYPE to trackMime,
|
KEY_MIME_TYPE to trackMime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// do not use `MediaFormat.KEY_TRACK_ID` as it is actually not unique between tracks
|
||||||
|
// e.g. there could be both a video track and an image track with KEY_TRACK_ID == 1
|
||||||
|
|
||||||
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 }
|
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 }
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
|
|
||||||
}
|
|
||||||
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
|
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
|
||||||
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
|
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
|
||||||
if (isVideo(trackMime)) {
|
if (isVideo(trackMime)) {
|
||||||
|
@ -626,25 +642,21 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
try {
|
try {
|
||||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input)
|
||||||
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
val fields = hashMapOf<String, Any?>(
|
||||||
try {
|
"projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT,
|
||||||
fun getIntProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null }
|
)
|
||||||
fun getStringProp(propName: String): String? = xmpDirs.map { it.xmpMeta.getPropertyString(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null }
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
val fields: FieldMap = hashMapOf(
|
val xmpMeta = dir.xmpMeta
|
||||||
"croppedAreaLeft" to getIntProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME),
|
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
|
||||||
"croppedAreaTop" to getIntProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME),
|
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
|
||||||
"croppedAreaWidth" to getIntProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME),
|
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
|
||||||
"croppedAreaHeight" to getIntProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME),
|
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
|
||||||
"fullPanoWidth" to getIntProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME),
|
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
|
||||||
"fullPanoHeight" to getIntProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME),
|
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
|
||||||
"projectionType" to (getStringProp(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT),
|
xmpMeta.getSafeString(XMP.GPANO_SCHEMA_NS, XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
|
||||||
)
|
|
||||||
result.success(fields)
|
|
||||||
return
|
|
||||||
} catch (e: XMPException) {
|
|
||||||
result.error("getPanoramaInfo-args", "failed to read XMP for uri=$uri", e.message)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
result.success(fields)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to read XMP", e)
|
Log.w(LOG_TAG, "failed to read XMP", e)
|
||||||
|
@ -875,7 +887,6 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
private const val KEY_HEIGHT = "height"
|
private const val KEY_HEIGHT = "height"
|
||||||
private const val KEY_WIDTH = "width"
|
private const val KEY_WIDTH = "width"
|
||||||
private const val KEY_PAGE = "page"
|
private const val KEY_PAGE = "page"
|
||||||
private const val KEY_TRACK_ID = "trackId"
|
|
||||||
private const val KEY_IS_DEFAULT = "isDefault"
|
private const val KEY_IS_DEFAULT = "isDefault"
|
||||||
private const val KEY_DURATION = "durationMillis"
|
private const val KEY_DURATION = "durationMillis"
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class MultiTrackImageGlideModule : LibraryGlideModule() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?)
|
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int?)
|
||||||
|
|
||||||
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, Bitmap> {
|
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, Bitmap> {
|
||||||
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||||
|
@ -52,9 +52,9 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int
|
||||||
|
|
||||||
val context = model.context
|
val context = model.context
|
||||||
val uri = model.uri
|
val uri = model.uri
|
||||||
val trackId = model.trackId
|
val trackIndex = model.trackIndex
|
||||||
|
|
||||||
val bitmap = MultiTrackMedia.getImage(context, uri, trackId)
|
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
callback.onLoadFailed(Exception("null bitmap"))
|
callback.onLoadFailed(Exception("null bitmap"))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,17 +16,17 @@ object MultiTrackMedia {
|
||||||
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)
|
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
fun getImage(context: Context, uri: Uri, trackId: Int?): Bitmap? {
|
fun getImage(context: Context, uri: Uri, trackIndex: Int?): Bitmap? {
|
||||||
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
|
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
|
||||||
try {
|
try {
|
||||||
return if (trackId != null) {
|
return if (trackIndex != null) {
|
||||||
val imageIndex = trackIdToImageIndex(context, uri, trackId) ?: return null
|
val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null
|
||||||
retriever.getImageAtIndex(imageIndex)
|
retriever.getImageAtIndex(imageIndex)
|
||||||
} else {
|
} else {
|
||||||
retriever.primaryImage
|
retriever.primaryImage
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackId=$trackId", e)
|
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex", e)
|
||||||
} finally {
|
} finally {
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
retriever.release()
|
retriever.release()
|
||||||
|
@ -34,7 +34,7 @@ object MultiTrackMedia {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trackIdToImageIndex(context: Context, uri: Uri, trackId: Int): Int? {
|
private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? {
|
||||||
val extractor = MediaExtractor()
|
val extractor = MediaExtractor()
|
||||||
try {
|
try {
|
||||||
extractor.setDataSource(context, uri, null)
|
extractor.setDataSource(context, uri, null)
|
||||||
|
@ -42,7 +42,7 @@ object MultiTrackMedia {
|
||||||
var imageIndex = 0
|
var imageIndex = 0
|
||||||
for (i in 0 until trackCount) {
|
for (i in 0 until trackCount) {
|
||||||
val trackFormat = extractor.getTrackFormat(i)
|
val trackFormat = extractor.getTrackFormat(i)
|
||||||
if (trackId == trackFormat.getInteger(MediaFormat.KEY_TRACK_ID)) {
|
if (trackIndex == i) {
|
||||||
return imageIndex
|
return imageIndex
|
||||||
}
|
}
|
||||||
if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) {
|
if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) {
|
||||||
|
@ -50,7 +50,7 @@ object MultiTrackMedia {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackId=$trackId", e)
|
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e)
|
||||||
} finally {
|
} finally {
|
||||||
extractor.release()
|
extractor.release()
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,15 @@ object TiffTags {
|
||||||
// Count = variable
|
// Count = variable
|
||||||
const val TAG_ORIGINAL_RAW_FILE_NAME = 0xc68b
|
const val TAG_ORIGINAL_RAW_FILE_NAME = 0xc68b
|
||||||
|
|
||||||
|
private val geotiffTags = listOf(
|
||||||
|
TAG_GEO_ASCII_PARAMS,
|
||||||
|
TAG_GEO_DOUBLE_PARAMS,
|
||||||
|
TAG_GEO_KEY_DIRECTORY,
|
||||||
|
TAG_MODEL_PIXEL_SCALE,
|
||||||
|
TAG_MODEL_TIEPOINT,
|
||||||
|
TAG_MODEL_TRANSFORMATION,
|
||||||
|
)
|
||||||
|
|
||||||
private val tagNameMap = hashMapOf(
|
private val tagNameMap = hashMapOf(
|
||||||
TAG_X_POSITION to "X Position",
|
TAG_X_POSITION to "X Position",
|
||||||
TAG_Y_POSITION to "Y Position",
|
TAG_Y_POSITION to "Y Position",
|
||||||
|
@ -132,6 +141,8 @@ object TiffTags {
|
||||||
TAG_ORIGINAL_RAW_FILE_NAME to "Original Raw File Name",
|
TAG_ORIGINAL_RAW_FILE_NAME to "Original Raw File Name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun isGeoTiffTag(tag: Int) = geotiffTags.contains(tag)
|
||||||
|
|
||||||
fun getTagName(tag: Int): String? {
|
fun getTagName(tag: Int): String? {
|
||||||
return tagNameMap[tag]
|
return tagNameMap[tag]
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,34 @@ object XMP {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun XMPMeta.getSafeInt(schema: String, propName: String, save: (value: Int) -> Unit) {
|
||||||
|
try {
|
||||||
|
if (doesPropertyExist(schema, propName)) {
|
||||||
|
val item = getPropertyInteger(schema, propName)
|
||||||
|
// double check retrieved items as the property sometimes is reported to exist but it is actually null
|
||||||
|
if (item != null) {
|
||||||
|
save(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: XMPException) {
|
||||||
|
Log.w(LOG_TAG, "failed to get int for XMP schema=$schema, propName=$propName", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun XMPMeta.getSafeString(schema: String, propName: String, save: (value: String) -> Unit) {
|
||||||
|
try {
|
||||||
|
if (doesPropertyExist(schema, propName)) {
|
||||||
|
val item = getPropertyString(schema, propName)
|
||||||
|
// double check retrieved items as the property sometimes is reported to exist but it is actually null
|
||||||
|
if (item != null) {
|
||||||
|
save(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: XMPException) {
|
||||||
|
Log.w(LOG_TAG, "failed to get int for XMP schema=$schema, propName=$propName", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun XMPMeta.getSafeLocalizedText(schema: String, propName: String, acceptBlank: Boolean = true, save: (value: String) -> Unit) {
|
fun XMPMeta.getSafeLocalizedText(schema: String, propName: String, acceptBlank: Boolean = true, save: (value: String) -> Unit) {
|
||||||
try {
|
try {
|
||||||
if (doesPropertyExist(schema, propName)) {
|
if (doesPropertyExist(schema, propName)) {
|
||||||
|
|
6
android/app/src/main/res/values-ko/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">아베스</string>
|
||||||
|
<string name="search_shortcut_short_label">검색</string>
|
||||||
|
<string name="videos_shortcut_short_label">동영상</string>
|
||||||
|
</resources>
|
4
android/app/src/profile/res/values-ko/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">아베스 [Profile]</string>
|
||||||
|
</resources>
|
|
@ -1,18 +1,21 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.4.30'
|
ext.kotlin_version = '1.4.31'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
// TODO TLAD remove jcenter (migrating to mavenCentral) when this is fixed: https://youtrack.jetbrains.com/issue/IDEA-261387
|
// TODO TLAD remove jcenter (migrating to mavenCentral) when this is fixed: https://youtrack.jetbrains.com/issue/IDEA-261387
|
||||||
jcenter()
|
jcenter {
|
||||||
|
content {
|
||||||
|
includeModule("org.jetbrains.trove4j", "trove4j")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
// TODO TLAD upgrade AGP to 4+ when this lands on stable: https://github.com/flutter/flutter/commit/8dd0de7f580972079f610a56a689b0a9c414f81e
|
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||||
classpath 'com.android.tools.build:gradle:3.6.4'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'com.google.gms:google-services:4.3.5'
|
classpath 'com.google.gms:google-services:4.3.5'
|
||||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.0'
|
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +24,11 @@ allprojects {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
// TODO TLAD remove jcenter (migrating to mavenCentral) when this is fixed: https://youtrack.jetbrains.com/issue/IDEA-261387
|
// TODO TLAD remove jcenter (migrating to mavenCentral) when this is fixed: https://youtrack.jetbrains.com/issue/IDEA-261387
|
||||||
jcenter()
|
jcenter {
|
||||||
|
content {
|
||||||
|
includeModule("org.jetbrains.trove4j", "trove4j")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// gradle.projectsEvaluated {
|
// gradle.projectsEvaluated {
|
||||||
// tasks.withType(JavaCompile) {
|
// tasks.withType(JavaCompile) {
|
||||||
|
|
|
@ -15,4 +15,3 @@ android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
android.enableR8=true
|
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
version="1.1"
|
|
||||||
id="svg16"
|
|
||||||
sodipodi:docname="aves_icon_20190726_0032.svg"
|
|
||||||
inkscape:export-filename="C:\Users\tibo\Downloads\aves0030.png"
|
|
||||||
inkscape:export-xdpi="98.300003"
|
|
||||||
inkscape:export-ydpi="98.300003"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
|
||||||
<metadata
|
|
||||||
id="metadata20">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1600"
|
|
||||||
inkscape:window-height="837"
|
|
||||||
id="namedview18"
|
|
||||||
showgrid="false"
|
|
||||||
fit-margin-top="68"
|
|
||||||
fit-margin-left="60"
|
|
||||||
fit-margin-right="60"
|
|
||||||
fit-margin-bottom="68"
|
|
||||||
inkscape:zoom="1.37"
|
|
||||||
inkscape:cx="288.67639"
|
|
||||||
inkscape:cy="258.4313"
|
|
||||||
inkscape:window-x="1358"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg16">
|
|
||||||
<inkscape:grid
|
|
||||||
type="xygrid"
|
|
||||||
id="grid28" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<defs
|
|
||||||
id="defs6">
|
|
||||||
<path
|
|
||||||
d="M 444.44,404.33 221.11,181 h 141.42 l 152.62,152.62 z"
|
|
||||||
id="a"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
d="m 346.96,501.78 -0.15,-0.15 V 332.22 l 84.86,84.85 z"
|
|
||||||
id="b"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
d="m 500.23,294.04 -56.56,-56.57 56.47,-56.47 0.09,0.1 z"
|
|
||||||
id="c"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
d="m 515.15,181 42.39,42.39 h -42.39 z"
|
|
||||||
id="d"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
id="g4567"
|
|
||||||
transform="translate(-11.68909,3.207375)">
|
|
||||||
<path
|
|
||||||
id="rect3756"
|
|
||||||
d="M 128.91211,145.45703 295.47266,312.01758 339.25,268.24219 216.46484,145.45703 Z"
|
|
||||||
style="fill:#3f51b5;fill-opacity:1;stroke-width:1.91993546"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="rect4589"
|
|
||||||
d="m 228.52199,267.34358 v 111.62418 l 55.81209,-55.81209 z"
|
|
||||||
style="fill:#4caf50;fill-opacity:1;stroke-width:1.2847122" />
|
|
||||||
<path
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="rect4589-5"
|
|
||||||
d="M 339.24927,246.24167 V 134.61749 l -55.81209,55.81209 z"
|
|
||||||
style="fill:#ffc107;fill-opacity:1;stroke-width:1.2847122" />
|
|
||||||
<path
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="rect4589-5-8"
|
|
||||||
d="m 394.46607,174.08259 -39.46511,-39.4651 v 39.4651 z"
|
|
||||||
style="fill:#ff5722;fill-opacity:1;stroke-width:0.6423561" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1,011 KiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 794 KiB |
Before Width: | Height: | Size: 404 KiB |
Before Width: | Height: | Size: 453 KiB |
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 3.4 MiB |
Before Width: | Height: | Size: 533 KiB |
Before Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 159 KiB |
Before Width: | Height: | Size: 2.8 MiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 3.8 MiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 320 KiB |
Before Width: | Height: | Size: 328 KiB |
Before Width: | Height: | Size: 3.2 MiB |
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 3.4 MiB |
Before Width: | Height: | Size: 914 KiB |
10
l10n.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# cf guide: http://flutter.dev/go/i18n-user-guide
|
||||||
|
|
||||||
|
# use defaults to:
|
||||||
|
# - parse ARB files from `lib/l10n`
|
||||||
|
# - generate class `AppLocalizations` in `app_localizations.dart`
|
||||||
|
|
||||||
|
preferred-supported-locales:
|
||||||
|
- en
|
||||||
|
|
||||||
|
# untranslated-messages-file: untranslated.json
|
9
lib/app_mode.dart
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
enum AppMode { main, pickExternal, pickInternal, view }
|
||||||
|
|
||||||
|
extension ExtraAppMode on AppMode {
|
||||||
|
bool get canSearch => this == AppMode.main || this == AppMode.pickExternal;
|
||||||
|
|
||||||
|
bool get hasDrawer => this == AppMode.main || this == AppMode.pickExternal;
|
||||||
|
|
||||||
|
bool get isPicking => this == AppMode.pickExternal || this == AppMode.pickInternal;
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui show Codec;
|
import 'dart:ui' as ui show Codec;
|
||||||
|
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
final mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
final pageId = key.pageId;
|
final pageId = key.pageId;
|
||||||
try {
|
try {
|
||||||
final bytes = await ImageFileService.getRegion(
|
final bytes = await imageFileService.getRegion(
|
||||||
uri,
|
uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
key.rotationDegrees,
|
key.rotationDegrees,
|
||||||
|
@ -55,11 +55,11 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
|
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
|
||||||
ImageFileService.resumeLoading(key);
|
imageFileService.resumeLoading(key);
|
||||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pause() => ImageFileService.cancelRegion(key);
|
void pause() => imageFileService.cancelRegion(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RegionProviderKey {
|
class RegionProviderKey {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:ui' as ui show Codec;
|
import 'dart:ui' as ui show Codec;
|
||||||
|
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
final mimeType = key.mimeType;
|
final mimeType = key.mimeType;
|
||||||
final pageId = key.pageId;
|
final pageId = key.pageId;
|
||||||
try {
|
try {
|
||||||
final bytes = await ImageFileService.getThumbnail(
|
final bytes = await imageFileService.getThumbnail(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
|
@ -55,11 +55,11 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
|
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
|
||||||
ImageFileService.resumeLoading(key);
|
imageFileService.resumeLoading(key);
|
||||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pause() => ImageFileService.cancelThumbnail(key);
|
void pause() => imageFileService.cancelThumbnail(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThumbnailProviderKey {
|
class ThumbnailProviderKey {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui' as ui show Codec;
|
import 'dart:ui' as ui show Codec;
|
||||||
|
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
@ -46,7 +46,7 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
assert(key == this);
|
assert(key == this);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final bytes = await ImageFileService.getImage(
|
final bytes = await imageFileService.getImage(
|
||||||
uri,
|
uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
rotationDegrees,
|
rotationDegrees,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
@ -8,13 +8,12 @@ class UriPicture extends PictureProvider<UriPicture> {
|
||||||
const UriPicture({
|
const UriPicture({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
this.colorFilter,
|
ColorFilter colorFilter,
|
||||||
}) : assert(uri != null);
|
}) : assert(uri != null),
|
||||||
|
super(colorFilter);
|
||||||
|
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
|
|
||||||
final ColorFilter colorFilter;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
|
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
|
||||||
return SynchronousFuture<UriPicture>(this);
|
return SynchronousFuture<UriPicture>(this);
|
||||||
|
@ -30,7 +29,7 @@ class UriPicture extends PictureProvider<UriPicture> {
|
||||||
Future<PictureInfo> _loadAsync(UriPicture key, {PictureErrorListener onError}) async {
|
Future<PictureInfo> _loadAsync(UriPicture key, {PictureErrorListener onError}) async {
|
||||||
assert(key == this);
|
assert(key == this);
|
||||||
|
|
||||||
final data = await ImageFileService.getSvg(uri, mimeType);
|
final data = await imageFileService.getSvg(uri, mimeType);
|
||||||
if (data == null || data.isEmpty) {
|
if (data == null || data.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
668
lib/l10n/app_en.arb
Normal file
|
@ -0,0 +1,668 @@
|
||||||
|
{
|
||||||
|
"appName": "Aves",
|
||||||
|
"@appName": {},
|
||||||
|
"welcomeMessage": "Welcome to Aves",
|
||||||
|
"@welcomeMessage": {},
|
||||||
|
"welcomeAnalyticsToggle": "Allow anonymous analytics and crash reporting (optional)",
|
||||||
|
"@welcomeAnalyticsToggle": {},
|
||||||
|
"welcomeTermsToggle": "I agree to the terms and conditions",
|
||||||
|
"@welcomeTermsToggle": {},
|
||||||
|
|
||||||
|
"applyButtonLabel": "APPLY",
|
||||||
|
"@applyButtonLabel": {},
|
||||||
|
"deleteButtonLabel": "DELETE",
|
||||||
|
"@deleteButtonLabel": {},
|
||||||
|
"hideButtonLabel": "HIDE",
|
||||||
|
"@hideButtonLabel": {},
|
||||||
|
"continueButtonLabel": "CONTINUE",
|
||||||
|
"@continueButtonLabel": {},
|
||||||
|
"clearTooltip": "Clear",
|
||||||
|
"@clearTooltip": {},
|
||||||
|
"previousTooltip": "Previous",
|
||||||
|
"@previousTooltip": {},
|
||||||
|
"nextTooltip": "Next",
|
||||||
|
"@nextTooltip": {},
|
||||||
|
|
||||||
|
"doubleBackExitMessage": "Tap “back” again to exit.",
|
||||||
|
"@doubleBackExitMessage": {},
|
||||||
|
|
||||||
|
"sourceStateLoading": "Loading",
|
||||||
|
"@sourceStateLoading": {},
|
||||||
|
"sourceStateCataloguing": "Cataloguing",
|
||||||
|
"@sourceStateCataloguing": {},
|
||||||
|
"sourceStateLocating": "Locating",
|
||||||
|
"@sourceStateLocating": {},
|
||||||
|
|
||||||
|
"chipActionDelete": "Delete",
|
||||||
|
"@chipActionDelete": {},
|
||||||
|
"chipActionGoToAlbumPage": "Show in Albums",
|
||||||
|
"@chipActionGoToAlbumPage": {},
|
||||||
|
"chipActionGoToCountryPage": "Show in Countries",
|
||||||
|
"@chipActionGoToCountryPage": {},
|
||||||
|
"chipActionGoToTagPage": "Show in Tags",
|
||||||
|
"@chipActionGoToTagPage": {},
|
||||||
|
"chipActionHide": "Hide",
|
||||||
|
"@chipActionHide": {},
|
||||||
|
"chipActionPin": "Pin to top",
|
||||||
|
"@chipActionPin": {},
|
||||||
|
"chipActionUnpin": "Unpin from top",
|
||||||
|
"@chipActionUnpin": {},
|
||||||
|
"chipActionRename": "Rename",
|
||||||
|
"@chipActionRename": {},
|
||||||
|
"chipActionSetCover": "Set cover",
|
||||||
|
"@chipActionSetCover": {},
|
||||||
|
|
||||||
|
"entryActionDelete": "Delete",
|
||||||
|
"@entryActionDelete": {},
|
||||||
|
"entryActionExport": "Export",
|
||||||
|
"@entryActionExport": {},
|
||||||
|
"entryActionInfo": "Info",
|
||||||
|
"@entryActionInfo": {},
|
||||||
|
"entryActionRename": "Rename",
|
||||||
|
"@entryActionRename": {},
|
||||||
|
"entryActionRotateCCW": "Rotate counterclockwise",
|
||||||
|
"@entryActionRotateCCW": {},
|
||||||
|
"entryActionRotateCW": "Rotate clockwise",
|
||||||
|
"@entryActionRotateCW": {},
|
||||||
|
"entryActionFlip": "Flip horizontally",
|
||||||
|
"@entryActionFlip": {},
|
||||||
|
"entryActionPrint": "Print",
|
||||||
|
"@entryActionPrint": {},
|
||||||
|
"entryActionShare": "Share",
|
||||||
|
"@entryActionShare": {},
|
||||||
|
"entryActionViewSource": "View source",
|
||||||
|
"@entryActionViewSource": {},
|
||||||
|
"entryActionEdit": "Edit with…",
|
||||||
|
"@entryActionEdit": {},
|
||||||
|
"entryActionOpen": "Open with…",
|
||||||
|
"@entryActionOpen": {},
|
||||||
|
"entryActionSetAs": "Set as…",
|
||||||
|
"@entryActionSetAs": {},
|
||||||
|
"entryActionOpenMap": "Show on map…",
|
||||||
|
"@entryActionOpenMap": {},
|
||||||
|
"entryActionAddFavourite": "Add to favourites",
|
||||||
|
"@entryActionAddFavourite": {},
|
||||||
|
"entryActionRemoveFavourite": "Remove from favourites",
|
||||||
|
"@entryActionRemoveFavourite": {},
|
||||||
|
|
||||||
|
"filterFavouriteLabel": "Favourite",
|
||||||
|
"@filterFavouriteLabel": {},
|
||||||
|
"filterLocationEmptyLabel": "Unlocated",
|
||||||
|
"@filterLocationEmptyLabel": {},
|
||||||
|
"filterTagEmptyLabel": "Untagged",
|
||||||
|
"@filterTagEmptyLabel": {},
|
||||||
|
"filterTypeAnimatedLabel": "Animated",
|
||||||
|
"@filterTypeAnimatedLabel": {},
|
||||||
|
"filterTypePanoramaLabel": "Panorama",
|
||||||
|
"@filterTypePanoramaLabel": {},
|
||||||
|
"filterTypeSphericalVideoLabel": "360° Video",
|
||||||
|
"@filterTypeSphericalVideoLabel": {},
|
||||||
|
"filterTypeGeotiffLabel": "GeoTIFF",
|
||||||
|
"@filterTypeGeotiffLabel": {},
|
||||||
|
"filterMimeImageLabel": "Image",
|
||||||
|
"@filterMimeImageLabel": {},
|
||||||
|
"filterMimeVideoLabel": "Video",
|
||||||
|
"@filterMimeVideoLabel": {},
|
||||||
|
|
||||||
|
"coordinateFormatDms": "DMS",
|
||||||
|
"@coordinateFormatDms": {},
|
||||||
|
"coordinateFormatDecimal": "Decimal degrees",
|
||||||
|
"@coordinateFormatDecimal": {},
|
||||||
|
|
||||||
|
"mapStyleGoogleNormal": "Google Maps",
|
||||||
|
"@mapStyleGoogleNormal": {},
|
||||||
|
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||||
|
"@mapStyleGoogleHybrid": {},
|
||||||
|
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
||||||
|
"@mapStyleGoogleTerrain": {},
|
||||||
|
"mapStyleOsmHot": "Humanitarian OSM",
|
||||||
|
"@mapStyleOsmHot": {},
|
||||||
|
"mapStyleStamenToner": "Stamen Toner",
|
||||||
|
"@mapStyleStamenToner": {},
|
||||||
|
"mapStyleStamenWatercolor": "Stamen Watercolor",
|
||||||
|
"@mapStyleStamenWatercolor": {},
|
||||||
|
|
||||||
|
"keepScreenOnNever": "Never",
|
||||||
|
"@keepScreenOnNever": {},
|
||||||
|
"keepScreenOnViewerOnly": "Viewer page only",
|
||||||
|
"@keepScreenOnViewerOnly": {},
|
||||||
|
"keepScreenOnAlways": "Always",
|
||||||
|
"@keepScreenOnAlways": {},
|
||||||
|
|
||||||
|
"albumTierPinned": "Pinned",
|
||||||
|
"@albumTierPinned": {},
|
||||||
|
"albumTierSpecial": "Common",
|
||||||
|
"@albumTierSpecial": {},
|
||||||
|
"albumTierApps": "Apps",
|
||||||
|
"@albumTierApps": {},
|
||||||
|
"albumTierRegular": "Others",
|
||||||
|
"@albumTierRegular": {},
|
||||||
|
|
||||||
|
"storageVolumeDescriptionFallbackPrimary": "Internal storage",
|
||||||
|
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||||
|
"storageVolumeDescriptionFallbackNonPrimary": "SD card",
|
||||||
|
"@storageVolumeDescriptionFallbackNonPrimary": {},
|
||||||
|
"rootDirectoryDescription": "root directory",
|
||||||
|
"@rootDirectoryDescription": {},
|
||||||
|
"otherDirectoryDescription": "“{name}” directory",
|
||||||
|
"@otherDirectoryDescription": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storageAccessDialogTitle": "Storage Access",
|
||||||
|
"@storageAccessDialogTitle": {},
|
||||||
|
"storageAccessDialogMessage": "Please select the {directory} of “{volume}” in the next screen to give this app access to it.",
|
||||||
|
"@storageAccessDialogMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"directory": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedAccessDialogTitle": "Restricted Access",
|
||||||
|
"@restrictedAccessDialogTitle": {},
|
||||||
|
"restrictedAccessDialogMessage": "This app is not allowed to modify files in the {directory} of “{volume}”.\n\nPlease use a pre-installed file manager or gallery app to move the items to another directory.",
|
||||||
|
"@restrictedAccessDialogMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"directory": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notEnoughSpaceDialogTitle": "Not Enough Space",
|
||||||
|
"@notEnoughSpaceDialogTitle": {},
|
||||||
|
"notEnoughSpaceDialogMessage": "This operation needs {neededSize} of free space on “{volume}” to complete, but there is only {freeSize} left.",
|
||||||
|
"@notEnoughSpaceDialogMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"neededSize": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"freeSize": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"addShortcutDialogLabel": "Shortcut label",
|
||||||
|
"@addShortcutDialogLabel": {},
|
||||||
|
"addShortcutButtonLabel": "ADD",
|
||||||
|
"@addShortcutButtonLabel": {},
|
||||||
|
|
||||||
|
"noMatchingAppDialogTitle": "No Matching App",
|
||||||
|
"@noMatchingAppDialogTitle": {},
|
||||||
|
"noMatchingAppDialogMessage": "There are no apps that can handle this.",
|
||||||
|
"@noMatchingAppDialogMessage": {},
|
||||||
|
|
||||||
|
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this item?} other{Are you sure you want to delete these {count} items?}}",
|
||||||
|
"@deleteEntriesConfirmationDialogMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"setCoverDialogTitle": "Set Cover",
|
||||||
|
"@setCoverDialogTitle": {},
|
||||||
|
"setCoverDialogLatest": "Latest item",
|
||||||
|
"@setCoverDialogLatest": {},
|
||||||
|
"setCoverDialogCustom": "Custom",
|
||||||
|
"@setCoverDialogCustom": {},
|
||||||
|
|
||||||
|
"hideFilterConfirmationDialogMessage": "Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?",
|
||||||
|
"@hideFilterConfirmationDialogMessage": {},
|
||||||
|
|
||||||
|
"newAlbumDialogTitle": "New Album",
|
||||||
|
"@newAlbumDialogTitle": {},
|
||||||
|
"newAlbumDialogNameLabel": "Album name",
|
||||||
|
"@newAlbumDialogNameLabel": {},
|
||||||
|
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
|
||||||
|
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||||
|
"newAlbumDialogStorageLabel": "Storage:",
|
||||||
|
"@newAlbumDialogStorageLabel": {},
|
||||||
|
|
||||||
|
"renameAlbumDialogLabel": "New name",
|
||||||
|
"@renameAlbumDialogLabel": {},
|
||||||
|
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
|
||||||
|
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||||
|
|
||||||
|
"deleteAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}",
|
||||||
|
"@deleteAlbumConfirmationDialogMessage": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"renameEntryDialogLabel": "New name",
|
||||||
|
"@renameEntryDialogLabel": {},
|
||||||
|
|
||||||
|
"genericSuccessFeedback": "Done!",
|
||||||
|
"@genericSuccessFeedback": {},
|
||||||
|
"genericFailureFeedback": "Failed",
|
||||||
|
"@genericFailureFeedback": {},
|
||||||
|
|
||||||
|
"menuActionSort": "Sort",
|
||||||
|
"@menuActionSort": {},
|
||||||
|
"menuActionGroup": "Group",
|
||||||
|
"@menuActionGroup": {},
|
||||||
|
"menuActionStats": "Stats",
|
||||||
|
"@menuActionStats": {},
|
||||||
|
|
||||||
|
"aboutPageTitle": "About",
|
||||||
|
"@aboutPageTitle": {},
|
||||||
|
"aboutFlutter": "Flutter",
|
||||||
|
"@aboutFlutter": {},
|
||||||
|
"aboutUpdate": "New Version Available",
|
||||||
|
"@aboutUpdate": {},
|
||||||
|
"aboutUpdateLinks1": "A new version of Aves is available on",
|
||||||
|
"@aboutUpdateLinks1": {},
|
||||||
|
"aboutUpdateLinks2": "and",
|
||||||
|
"@aboutUpdateLinks2": {},
|
||||||
|
"aboutUpdateLinks3": ".",
|
||||||
|
"@aboutUpdateLinks3": {},
|
||||||
|
"aboutUpdateGithub": "Github",
|
||||||
|
"@aboutUpdateGithub": {},
|
||||||
|
"aboutUpdateGooglePlay": "Google Play",
|
||||||
|
"@aboutUpdateGooglePlay": {},
|
||||||
|
"aboutCredits": "Credits",
|
||||||
|
"@aboutCredits": {},
|
||||||
|
"aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from",
|
||||||
|
"@aboutCreditsWorldAtlas1": {},
|
||||||
|
"aboutCreditsWorldAtlas2": "under ISC License.",
|
||||||
|
"@aboutCreditsWorldAtlas2": {},
|
||||||
|
"aboutLicenses": "Open-Source Licenses",
|
||||||
|
"@aboutLicenses": {},
|
||||||
|
"aboutLicensesBanner": "This app uses the following open-source packages and libraries.",
|
||||||
|
"@aboutLicensesBanner": {},
|
||||||
|
"aboutLicensesSortTooltip": "Sort",
|
||||||
|
"@aboutLicensesSortTooltip": {},
|
||||||
|
"aboutLicensesSortByName": "Sort by name",
|
||||||
|
"@aboutLicensesSortByName": {},
|
||||||
|
"aboutLicensesSortByLicense": "Sort by license",
|
||||||
|
"@aboutLicensesSortByLicense": {},
|
||||||
|
"aboutLicensesAndroidLibraries": "Android Libraries",
|
||||||
|
"@aboutLicensesAndroidLibraries": {},
|
||||||
|
"aboutLicensesFlutterPlugins": "Flutter Plugins",
|
||||||
|
"@aboutLicensesFlutterPlugins": {},
|
||||||
|
"aboutLicensesFlutterPackages": "Flutter Packages",
|
||||||
|
"@aboutLicensesFlutterPackages": {},
|
||||||
|
"aboutLicensesDartPackages": "Dart Packages",
|
||||||
|
"@aboutLicensesDartPackages": {},
|
||||||
|
"aboutLicensesShowAllButtonLabel": "SHOW ALL LICENSES",
|
||||||
|
"@aboutLicensesShowAllButtonLabel": {},
|
||||||
|
|
||||||
|
"collectionPageTitle": "Collection",
|
||||||
|
"@collectionPageTitle": {},
|
||||||
|
"collectionPickPageTitle": "Pick",
|
||||||
|
"@collectionPickPageTitle": {},
|
||||||
|
"collectionSelectionPageTitle": "{count, plural, =0{Select items} =1{1 item} other{{count} items}}",
|
||||||
|
"@collectionSelectionPageTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"collectionActionAddShortcut": "Add shortcut",
|
||||||
|
"@collectionActionAddShortcut": {},
|
||||||
|
"collectionActionSelect": "Select",
|
||||||
|
"@collectionActionSelect": {},
|
||||||
|
"collectionActionSelectAll": "Select all",
|
||||||
|
"@collectionActionSelectAll": {},
|
||||||
|
"collectionActionSelectNone": "Select none",
|
||||||
|
"@collectionActionSelectNone": {},
|
||||||
|
"collectionActionCopy": "Copy to album",
|
||||||
|
"@collectionActionCopy": {},
|
||||||
|
"collectionActionMove": "Move to album",
|
||||||
|
"@collectionActionMove": {},
|
||||||
|
"collectionActionRefreshMetadata": "Refresh metadata",
|
||||||
|
"@collectionActionRefreshMetadata": {},
|
||||||
|
|
||||||
|
"collectionSortTitle": "Sort",
|
||||||
|
"@collectionSortTitle": {},
|
||||||
|
"collectionSortDate": "By date",
|
||||||
|
"@collectionSortDate": {},
|
||||||
|
"collectionSortSize": "By size",
|
||||||
|
"@collectionSortSize": {},
|
||||||
|
"collectionSortName": "By album & file name",
|
||||||
|
"@collectionSortName": {},
|
||||||
|
|
||||||
|
"collectionGroupTitle": "Group",
|
||||||
|
"@collectionGroupTitle": {},
|
||||||
|
"collectionGroupAlbum": "By album",
|
||||||
|
"@collectionGroupAlbum": {},
|
||||||
|
"collectionGroupMonth": "By month",
|
||||||
|
"@collectionGroupMonth": {},
|
||||||
|
"collectionGroupDay": "By day",
|
||||||
|
"@collectionGroupDay": {},
|
||||||
|
"collectionGroupNone": "Do not group",
|
||||||
|
"@collectionGroupNone": {},
|
||||||
|
|
||||||
|
"sectionUnknown": "Unknown",
|
||||||
|
"@sectionUnknown": {},
|
||||||
|
"dateToday": "Today",
|
||||||
|
"@dateToday": {},
|
||||||
|
"dateYesterday": "Yesterday",
|
||||||
|
"@dateYesterday": {},
|
||||||
|
"dateThisMonth": "This month",
|
||||||
|
"@dateThisMonth": {},
|
||||||
|
"errorUnsupportedMimeType": "{mimeType} not supported",
|
||||||
|
"@errorUnsupportedMimeType": {
|
||||||
|
"placeholders": {
|
||||||
|
"mimeType": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collectionDeleteFailureFeedback": "{count, plural, =1{Failed to delete 1 item} other{Failed to delete {count} items}}",
|
||||||
|
"@collectionDeleteFailureFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collectionCopyFailureFeedback": "{count, plural, =1{Failed to copy 1 item} other{Failed to copy {count} items}}",
|
||||||
|
"@collectionCopyFailureFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collectionMoveFailureFeedback": "{count, plural, =1{Failed to move 1 item} other{Failed to move {count} items}}",
|
||||||
|
"@collectionMoveFailureFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collectionExportFailureFeedback": "{count, plural, =1{Failed to export 1 page} other{Failed to export {count} pages}}",
|
||||||
|
"@collectionExportFailureFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collectionCopySuccessFeedback": "{count, plural, =1{Copied 1 item} other{Copied {count} items}}",
|
||||||
|
"@collectionCopySuccessFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"collectionMoveSuccessFeedback": "{count, plural, =1{Moved 1 item} other{Moved {count} items}}",
|
||||||
|
"@collectionMoveSuccessFeedback": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"collectionEmptyFavourites": "No favourites",
|
||||||
|
"@collectionEmptyFavourites": {},
|
||||||
|
"collectionEmptyVideos": "No videos",
|
||||||
|
"@collectionEmptyVideos": {},
|
||||||
|
"collectionEmptyImages": "No images",
|
||||||
|
"@collectionEmptyImages": {},
|
||||||
|
|
||||||
|
"collectionSelectSectionTooltip": "Select section",
|
||||||
|
"@collectionSelectSectionTooltip": {},
|
||||||
|
"collectionDeselectSectionTooltip": "Deselect section",
|
||||||
|
"@collectionDeselectSectionTooltip": {},
|
||||||
|
|
||||||
|
"drawerCollectionAll": "All collection",
|
||||||
|
"@drawerCollectionAll": {},
|
||||||
|
"drawerCollectionVideos": "Videos",
|
||||||
|
"@drawerCollectionVideos": {},
|
||||||
|
"drawerCollectionFavourites": "Favourites",
|
||||||
|
"@drawerCollectionFavourites": {},
|
||||||
|
|
||||||
|
"chipSortTitle": "Sort",
|
||||||
|
"@chipSortTitle": {},
|
||||||
|
"chipSortDate": "By date",
|
||||||
|
"@chipSortDate": {},
|
||||||
|
"chipSortName": "By name",
|
||||||
|
"@chipSortName": {},
|
||||||
|
"chipSortCount": "By item count",
|
||||||
|
"@chipSortCount": {},
|
||||||
|
|
||||||
|
"albumGroupTitle": "Group",
|
||||||
|
"@albumGroupTitle": {},
|
||||||
|
"albumGroupTier": "By tier",
|
||||||
|
"@albumGroupTier": {},
|
||||||
|
"albumGroupVolume": "By storage volume",
|
||||||
|
"@albumGroupVolume": {},
|
||||||
|
"albumGroupNone": "Do not group",
|
||||||
|
"@albumGroupNone": {},
|
||||||
|
|
||||||
|
"albumPickPageTitleCopy": "Copy to Album",
|
||||||
|
"@albumPickPageTitleCopy": {},
|
||||||
|
"albumPickPageTitleExport": "Export to Album",
|
||||||
|
"@albumPickPageTitleExport": {},
|
||||||
|
"albumPickPageTitleMove": "Move to Album",
|
||||||
|
"@albumPickPageTitleMove": {},
|
||||||
|
|
||||||
|
"albumPageTitle": "Albums",
|
||||||
|
"@albumPageTitle": {},
|
||||||
|
"albumEmpty": "No albums",
|
||||||
|
"@albumEmpty": {},
|
||||||
|
"createAlbumTooltip": "Create album",
|
||||||
|
"@createAlbumTooltip": {},
|
||||||
|
"createAlbumButtonLabel": "CREATE",
|
||||||
|
"@createAlbumButtonLabel": {},
|
||||||
|
|
||||||
|
"countryPageTitle": "Countries",
|
||||||
|
"@countryPageTitle": {},
|
||||||
|
"countryEmpty": "No countries",
|
||||||
|
"@countryEmpty": {},
|
||||||
|
|
||||||
|
"tagPageTitle": "Tags",
|
||||||
|
"@tagPageTitle": {},
|
||||||
|
"tagEmpty": "No tags",
|
||||||
|
"@tagEmpty": {},
|
||||||
|
|
||||||
|
"searchCollectionFieldHint": "Search collection",
|
||||||
|
"@searchCollectionFieldHint": {},
|
||||||
|
"searchSectionRecent": "Recent",
|
||||||
|
"@searchSectionRecent": {},
|
||||||
|
"searchSectionAlbums": "Albums",
|
||||||
|
"@searchSectionAlbums": {},
|
||||||
|
"searchSectionCountries": "Countries",
|
||||||
|
"@searchSectionCountries": {},
|
||||||
|
"searchSectionPlaces": "Places",
|
||||||
|
"@searchSectionPlaces": {},
|
||||||
|
"searchSectionTags": "Tags",
|
||||||
|
"@searchSectionTags": {},
|
||||||
|
|
||||||
|
"settingsPageTitle": "Settings",
|
||||||
|
"@settingsPageTitle": {},
|
||||||
|
"settingsSystemDefault": "System",
|
||||||
|
"@settingsSystemDefault": {},
|
||||||
|
|
||||||
|
"settingsSectionNavigation": "Navigation",
|
||||||
|
"@settingsSectionNavigation": {},
|
||||||
|
"settingsHome": "Home",
|
||||||
|
"@settingsHome": {},
|
||||||
|
"settingsDoubleBackExit": "Tap “back” twice to exit",
|
||||||
|
"@settingsDoubleBackExit": {},
|
||||||
|
|
||||||
|
"settingsSectionDisplay": "Display",
|
||||||
|
"@settingsSectionDisplay": {},
|
||||||
|
"settingsLanguage": "Language",
|
||||||
|
"@settingsLanguage": {},
|
||||||
|
"settingsKeepScreenOnTile": "Keep screen on",
|
||||||
|
"@settingsKeepScreenOnTile": {},
|
||||||
|
"settingsKeepScreenOnTitle": "Keep Screen On",
|
||||||
|
"@settingsKeepScreenOnTitle": {},
|
||||||
|
"settingsRasterImageBackground": "Raster image background",
|
||||||
|
"@settingsRasterImageBackground": {},
|
||||||
|
"settingsVectorImageBackground": "Vector image background",
|
||||||
|
"@settingsVectorImageBackground": {},
|
||||||
|
"settingsCoordinateFormatTile": "Coordinate format",
|
||||||
|
"@settingsCoordinateFormatTile": {},
|
||||||
|
"settingsCoordinateFormatTitle": "Coordinate Format",
|
||||||
|
"@settingsCoordinateFormatTitle": {},
|
||||||
|
|
||||||
|
"settingsSectionThumbnails": "Thumbnails",
|
||||||
|
"@settingsSectionThumbnails": {},
|
||||||
|
"settingsThumbnailShowLocationIcon": "Show location icon",
|
||||||
|
"@settingsThumbnailShowLocationIcon": {},
|
||||||
|
"settingsThumbnailShowRawIcon": "Show raw icon",
|
||||||
|
"@settingsThumbnailShowRawIcon": {},
|
||||||
|
"settingsThumbnailShowVideoDuration": "Show video duration",
|
||||||
|
"@settingsThumbnailShowVideoDuration": {},
|
||||||
|
|
||||||
|
"settingsSectionViewer": "Viewer",
|
||||||
|
"@settingsSectionViewer": {},
|
||||||
|
"settingsViewerShowMinimap": "Show minimap",
|
||||||
|
"@settingsViewerShowMinimap": {},
|
||||||
|
"settingsViewerShowInformation": "Show information",
|
||||||
|
"@settingsViewerShowInformation": {},
|
||||||
|
"settingsViewerShowInformationSubtitle": "Show title, date, location, etc.",
|
||||||
|
"@settingsViewerShowInformationSubtitle": {},
|
||||||
|
"settingsViewerShowShootingDetails": "Show shooting details",
|
||||||
|
"@settingsViewerShowShootingDetails": {},
|
||||||
|
|
||||||
|
"settingsSectionSearch": "Search",
|
||||||
|
"@settingsSectionSearch": {},
|
||||||
|
"settingsSaveSearchHistory": "Save search history",
|
||||||
|
"@settingsSaveSearchHistory": {},
|
||||||
|
|
||||||
|
"settingsSectionPrivacy": "Privacy",
|
||||||
|
"@settingsSectionPrivacy": {},
|
||||||
|
"settingsEnableAnalytics": "Allow anonymous analytics and crash reporting",
|
||||||
|
"@settingsEnableAnalytics": {},
|
||||||
|
|
||||||
|
"settingsHiddenFiltersTile": "Hidden filters",
|
||||||
|
"@settingsHiddenFiltersTile": {},
|
||||||
|
"settingsHiddenFiltersTitle": "Hidden Filters",
|
||||||
|
"@settingsHiddenFiltersTitle": {},
|
||||||
|
"settingsHiddenFiltersBanner": "Photos and videos matching hidden filters will not appear in your collection.",
|
||||||
|
"@settingsHiddenFiltersBanner": {},
|
||||||
|
"settingsHiddenFiltersEmpty": "No hidden filters",
|
||||||
|
"@settingsHiddenFiltersEmpty": {},
|
||||||
|
|
||||||
|
"settingsStorageAccessTile": "Storage access",
|
||||||
|
"@settingsStorageAccessTile": {},
|
||||||
|
"settingsStorageAccessTitle": "Storage Access",
|
||||||
|
"@settingsStorageAccessTitle": {},
|
||||||
|
"settingsStorageAccessBanner": "Some directories require an explicit access grant to modify files in them. You can review here directories to which you previously gave access.",
|
||||||
|
"@settingsStorageAccessBanner": {},
|
||||||
|
"settingsStorageAccessEmpty": "No access grants",
|
||||||
|
"@settingsStorageAccessEmpty": {},
|
||||||
|
"settingsStorageAccessRevokeTooltip": "Revoke",
|
||||||
|
"@settingsStorageAccessRevokeTooltip": {},
|
||||||
|
|
||||||
|
"statsPageTitle": "Stats",
|
||||||
|
"@statsPageTitle": {},
|
||||||
|
"statsImage": "{count, plural, =1{image} other{images}}",
|
||||||
|
"@statsImage": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"statsVideo": "{count, plural, =1{video} other{videos}}",
|
||||||
|
"@statsVideo": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
||||||
|
"@statsWithGps": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"statsTopCountries": "Top Countries",
|
||||||
|
"@statsTopCountries": {},
|
||||||
|
"statsTopPlaces": "Top Places",
|
||||||
|
"@statsTopPlaces": {},
|
||||||
|
"statsTopTags": "Top Tags",
|
||||||
|
"@statsTopTags": {},
|
||||||
|
|
||||||
|
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
|
||||||
|
"@viewerOpenPanoramaButtonLabel": {},
|
||||||
|
"viewerOpenTooltip": "Open",
|
||||||
|
"@viewerOpenTooltip": {},
|
||||||
|
"viewerPauseTooltip": "Pause",
|
||||||
|
"@viewerPauseTooltip": {},
|
||||||
|
"viewerPlayTooltip": "Play",
|
||||||
|
"@viewerPlayTooltip": {},
|
||||||
|
"viewerErrorUnknown": "Oops!",
|
||||||
|
"@viewerErrorUnknown": {},
|
||||||
|
"viewerErrorDoesNotExist": "The file no longer exists.",
|
||||||
|
"@viewerErrorDoesNotExist": {},
|
||||||
|
|
||||||
|
"viewerInfoPageTitle": "Info",
|
||||||
|
"@viewerInfoPageTitle": {},
|
||||||
|
"viewerInfoBackToViewerTooltip": "Back to viewer",
|
||||||
|
"@viewerInfoBackToViewerTooltip": {},
|
||||||
|
|
||||||
|
"viewerInfoUnknown": "unknown",
|
||||||
|
"@viewerInfoUnknown": {},
|
||||||
|
"viewerInfoLabelTitle": "Title",
|
||||||
|
"@viewerInfoLabelTitle": {},
|
||||||
|
"viewerInfoLabelDate": "Date",
|
||||||
|
"@viewerInfoLabelDate": {},
|
||||||
|
"viewerInfoLabelResolution": "Resolution",
|
||||||
|
"@viewerInfoLabelResolution": {},
|
||||||
|
"viewerInfoLabelSize": "Size",
|
||||||
|
"@viewerInfoLabelSize": {},
|
||||||
|
"viewerInfoLabelUri": "URI",
|
||||||
|
"@viewerInfoLabelUri": {},
|
||||||
|
"viewerInfoLabelPath": "Path",
|
||||||
|
"@viewerInfoLabelPath": {},
|
||||||
|
"viewerInfoLabelDuration": "Duration",
|
||||||
|
"@viewerInfoLabelDuration": {},
|
||||||
|
"viewerInfoLabelOwner": "Owned by",
|
||||||
|
"@viewerInfoLabelOwner": {},
|
||||||
|
"viewerInfoLabelCoordinates": "Coordinates",
|
||||||
|
"@viewerInfoLabelCoordinates": {},
|
||||||
|
"viewerInfoLabelAddress": "Address",
|
||||||
|
"@viewerInfoLabelAddress": {},
|
||||||
|
|
||||||
|
"viewerInfoMapStyleTitle": "Map Style",
|
||||||
|
"@viewerInfoMapStyleTitle": {},
|
||||||
|
"viewerInfoMapStyleTooltip": "Select map style",
|
||||||
|
"@viewerInfoMapStyleTooltip": {},
|
||||||
|
"viewerInfoMapZoomInTooltip": "Zoom in",
|
||||||
|
"@viewerInfoMapZoomInTooltip": {},
|
||||||
|
"viewerInfoMapZoomOutTooltip": "Zoom out",
|
||||||
|
"@viewerInfoMapZoomOutTooltip": {},
|
||||||
|
"mapAttributionOsmHot": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [HOT](https://www.hotosm.org/) • Hosted by [OSM France](https://openstreetmap.fr/)",
|
||||||
|
"@mapAttributionOsmHot": {},
|
||||||
|
"mapAttributionStamen": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
|
||||||
|
"@mapAttributionStamen": {},
|
||||||
|
|
||||||
|
"viewerInfoOpenEmbeddedFailureFeedback": "Failed to extract embedded data",
|
||||||
|
"@viewerInfoOpenEmbeddedFailureFeedback": {},
|
||||||
|
"viewerInfoOpenLinkText": "Open",
|
||||||
|
"@viewerInfoOpenLinkText": {},
|
||||||
|
"viewerInfoViewXmlLinkText": "View XML",
|
||||||
|
"@viewerInfoViewXmlLinkText": {},
|
||||||
|
|
||||||
|
"viewerInfoSearchFieldLabel": "Search metadata",
|
||||||
|
"@viewerInfoSearchFieldLabel": {},
|
||||||
|
"viewerInfoSearchEmpty": "No matching keys",
|
||||||
|
"@viewerInfoSearchEmpty": {},
|
||||||
|
"viewerInfoSearchSuggestionDate": "Date & time",
|
||||||
|
"@viewerInfoSearchSuggestionDate": {},
|
||||||
|
"viewerInfoSearchSuggestionDescription": "Description",
|
||||||
|
"@viewerInfoSearchSuggestionDescription": {},
|
||||||
|
"viewerInfoSearchSuggestionDimensions": "Dimensions",
|
||||||
|
"@viewerInfoSearchSuggestionDimensions": {},
|
||||||
|
"viewerInfoSearchSuggestionResolution": "Resolution",
|
||||||
|
"@viewerInfoSearchSuggestionResolution": {},
|
||||||
|
"viewerInfoSearchSuggestionRights": "Rights",
|
||||||
|
"@viewerInfoSearchSuggestionRights": {},
|
||||||
|
|
||||||
|
"panoramaEnableSensorControl": "Enable sensor control",
|
||||||
|
"@panoramaEnableSensorControl": {},
|
||||||
|
"panoramaDisableSensorControl": "Disable sensor control",
|
||||||
|
"@panoramaDisableSensorControl": {},
|
||||||
|
|
||||||
|
"sourceViewerPageTitle": "Source",
|
||||||
|
"@sourceViewerPageTitle": {}
|
||||||
|
}
|
318
lib/l10n/app_ko.arb
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
{
|
||||||
|
"appName": "아베스",
|
||||||
|
"welcomeMessage": "아베스 사용을 환영합니다",
|
||||||
|
"welcomeAnalyticsToggle": "진단 데이터를 보내는 것에 동의합니다 (선택)",
|
||||||
|
"welcomeTermsToggle": "이용약관에 동의합니다",
|
||||||
|
|
||||||
|
"applyButtonLabel": "확인",
|
||||||
|
"deleteButtonLabel": "삭제",
|
||||||
|
"hideButtonLabel": "숨기기",
|
||||||
|
"continueButtonLabel": "다음",
|
||||||
|
"clearTooltip": "초기화",
|
||||||
|
"previousTooltip": "이전",
|
||||||
|
"nextTooltip": "다음",
|
||||||
|
|
||||||
|
"doubleBackExitMessage": "종료하려면 한번 더 누르세요.",
|
||||||
|
|
||||||
|
"sourceStateLoading": "로딩 중",
|
||||||
|
"sourceStateCataloguing": "분석 중",
|
||||||
|
"sourceStateLocating": "장소 찾는 중",
|
||||||
|
|
||||||
|
"chipActionDelete": "삭제",
|
||||||
|
"chipActionGoToAlbumPage": "앨범 페이지에서 보기",
|
||||||
|
"chipActionGoToCountryPage": "국가 페이지에서 보기",
|
||||||
|
"chipActionGoToTagPage": "태그 페이지에서 보기",
|
||||||
|
"chipActionHide": "숨기기",
|
||||||
|
"chipActionPin": "고정",
|
||||||
|
"chipActionUnpin": "고정 해제",
|
||||||
|
"chipActionRename": "이름 변경",
|
||||||
|
"chipActionSetCover": "대표 이미지 변경",
|
||||||
|
|
||||||
|
"entryActionDelete": "삭제",
|
||||||
|
"entryActionExport": "내보내기",
|
||||||
|
"entryActionInfo": "상세정보",
|
||||||
|
"entryActionRename": "이름 변경",
|
||||||
|
"entryActionRotateCCW": "좌회전",
|
||||||
|
"entryActionRotateCW": "우회전",
|
||||||
|
"entryActionFlip": "좌우 뒤집기",
|
||||||
|
"entryActionPrint": "인쇄",
|
||||||
|
"entryActionShare": "공유",
|
||||||
|
"entryActionViewSource": "소스 코드 보기",
|
||||||
|
"entryActionEdit": "편집…",
|
||||||
|
"entryActionOpen": "다른 앱에서 열기…",
|
||||||
|
"entryActionSetAs": "다음 용도로 사용…",
|
||||||
|
"entryActionOpenMap": "지도에서 보기…",
|
||||||
|
"entryActionAddFavourite": "즐겨찾기에 추가",
|
||||||
|
"entryActionRemoveFavourite": "즐겨찾기에서 삭제",
|
||||||
|
|
||||||
|
"filterFavouriteLabel": "즐겨찾기",
|
||||||
|
"filterLocationEmptyLabel": "장소 없음",
|
||||||
|
"filterTagEmptyLabel": "태그 없음",
|
||||||
|
"filterTypeAnimatedLabel": "애니메이션",
|
||||||
|
"filterTypePanoramaLabel": "파노라마",
|
||||||
|
"filterTypeSphericalVideoLabel": "360° 동영상",
|
||||||
|
"filterTypeGeotiffLabel": "GeoTIFF",
|
||||||
|
"filterMimeImageLabel": "사진",
|
||||||
|
"filterMimeVideoLabel": "동영상",
|
||||||
|
|
||||||
|
"coordinateFormatDms": "도분초",
|
||||||
|
"coordinateFormatDecimal": "소수점",
|
||||||
|
|
||||||
|
"mapStyleGoogleNormal": "구글 지도",
|
||||||
|
"mapStyleGoogleHybrid": "구글 지도 (위성)",
|
||||||
|
"mapStyleGoogleTerrain": "구글 지도 (지형)",
|
||||||
|
"mapStyleOsmHot": "Humanitarian OSM",
|
||||||
|
"mapStyleStamenToner": "Stamen 토너",
|
||||||
|
"mapStyleStamenWatercolor": "Stamen 수채화",
|
||||||
|
|
||||||
|
"keepScreenOnNever": "자동 꺼짐",
|
||||||
|
"keepScreenOnViewerOnly": "뷰어 이용 시 작동",
|
||||||
|
"keepScreenOnAlways": "항상 켜짐",
|
||||||
|
|
||||||
|
"albumTierPinned": "고정",
|
||||||
|
"albumTierSpecial": "기본",
|
||||||
|
"albumTierApps": "앱",
|
||||||
|
"albumTierRegular": "일반",
|
||||||
|
|
||||||
|
"storageVolumeDescriptionFallbackPrimary": "내장 메모리",
|
||||||
|
"storageVolumeDescriptionFallbackNonPrimary": "SD 카드",
|
||||||
|
"rootDirectoryDescription": "루트 폴더",
|
||||||
|
"otherDirectoryDescription": "“{name}” 폴더",
|
||||||
|
"storageAccessDialogTitle": "저장공간 접근",
|
||||||
|
"storageAccessDialogMessage": "파일에 접근하도록 다음 화면에서 “{volume}”의 {directory}를 선택하세요.",
|
||||||
|
"restrictedAccessDialogTitle": "접근 제한",
|
||||||
|
"restrictedAccessDialogMessage": "“{volume}”의 {directory}에 있는 파일의 접근이 제한됩니다.\n\n기본으로 설치된 갤러리나 파일 관리 앱을 사용해서 다른 폴더로 파일을 이동하세요.",
|
||||||
|
"notEnoughSpaceDialogTitle": "저장공간 부족",
|
||||||
|
"notEnoughSpaceDialogMessage": "“{volume}”에 필요 공간은 {neededSize}인데 사용 가능한 용량은 {freeSize}만 남아있습니다.",
|
||||||
|
|
||||||
|
"addShortcutDialogLabel": "바로가기 라벨",
|
||||||
|
"addShortcutButtonLabel": "추가",
|
||||||
|
|
||||||
|
"noMatchingAppDialogTitle": "처리할 앱 없음",
|
||||||
|
"noMatchingAppDialogMessage": "이 작업을 처리할 수 있는 앱이 없습니다.",
|
||||||
|
|
||||||
|
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 삭제하시겠습니까?} other{항목 {count}개를 삭제하시겠습니까?}}",
|
||||||
|
|
||||||
|
"setCoverDialogTitle": "대표 이미지 변경",
|
||||||
|
"setCoverDialogLatest": "최근 항목",
|
||||||
|
"setCoverDialogCustom": "직접 설정",
|
||||||
|
|
||||||
|
"hideFilterConfirmationDialogMessage": "이 필터에 맞는 사진과 동영상이 보이지 않을 것입니다. “개인정보 보호” 설정을 수정하면 다시 보일 수 있습니다.\n\n이 필터를 숨기시겠습니까?",
|
||||||
|
|
||||||
|
"newAlbumDialogTitle": "새 앨범 만들기",
|
||||||
|
"newAlbumDialogNameLabel": "앨범 이름",
|
||||||
|
"newAlbumDialogNameLabelAlreadyExistsHelper": "사용 중인 이름입니다",
|
||||||
|
"newAlbumDialogStorageLabel": "저장공간:",
|
||||||
|
|
||||||
|
"renameAlbumDialogLabel": "앨범 이름",
|
||||||
|
"renameAlbumDialogLabelAlreadyExistsHelper": "사용 중인 이름입니다",
|
||||||
|
|
||||||
|
"deleteAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||||
|
|
||||||
|
"renameEntryDialogLabel": "이름",
|
||||||
|
|
||||||
|
"genericSuccessFeedback": "정상 처리됐습니다",
|
||||||
|
"genericFailureFeedback": "오류가 발생했습니다",
|
||||||
|
|
||||||
|
"menuActionSort": "정렬",
|
||||||
|
"menuActionGroup": "묶음",
|
||||||
|
"menuActionStats": "통계",
|
||||||
|
|
||||||
|
"aboutPageTitle": "앱 정보",
|
||||||
|
"aboutFlutter": "플러터",
|
||||||
|
"aboutUpdate": "업데이트 사용 가능",
|
||||||
|
"aboutUpdateLinks1": "앱의 최신 버전을",
|
||||||
|
"aboutUpdateLinks2": "와",
|
||||||
|
"aboutUpdateLinks3": "에서 다운로드 사용 가능합니다.",
|
||||||
|
"aboutUpdateGithub": "깃허브",
|
||||||
|
"aboutUpdateGooglePlay": "구글 플레이",
|
||||||
|
"aboutCredits": "크레딧",
|
||||||
|
"aboutCreditsWorldAtlas1": "이 앱은",
|
||||||
|
"aboutCreditsWorldAtlas2": "의 TopoJSON 파일(ISC 라이선스)을 이용합니다.",
|
||||||
|
"aboutLicenses": "오픈 소스 라이선스",
|
||||||
|
"aboutLicensesBanner": "이 앱은 다음의 오픈 소스 패키지와 라이브러리를 이용합니다.",
|
||||||
|
"aboutLicensesSortTooltip": "정렬",
|
||||||
|
"aboutLicensesSortByName": "이름순 정렬",
|
||||||
|
"aboutLicensesSortByLicense": "라이선스순 정렬",
|
||||||
|
"aboutLicensesAndroidLibraries": "안드로이드 라이브러리",
|
||||||
|
"aboutLicensesFlutterPlugins": "플러터 플러그인",
|
||||||
|
"aboutLicensesFlutterPackages": "플러터 패키지",
|
||||||
|
"aboutLicensesDartPackages": "다트 패키지",
|
||||||
|
"aboutLicensesShowAllButtonLabel": "라이선스 모두 보기",
|
||||||
|
|
||||||
|
"collectionPageTitle": "미디어",
|
||||||
|
"collectionPickPageTitle": "항목 선택",
|
||||||
|
"collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}",
|
||||||
|
|
||||||
|
"collectionActionAddShortcut": "홈 화면에 추가",
|
||||||
|
"collectionActionSelect": "선택",
|
||||||
|
"collectionActionSelectAll": "모두 선택",
|
||||||
|
"collectionActionSelectNone": "모두 해제",
|
||||||
|
"collectionActionCopy": "앨범으로 복사",
|
||||||
|
"collectionActionMove": "앨범으로 이동",
|
||||||
|
"collectionActionRefreshMetadata": "새로 분석",
|
||||||
|
|
||||||
|
"collectionSortTitle": "정렬",
|
||||||
|
"collectionSortDate": "날짜",
|
||||||
|
"collectionSortSize": "크기",
|
||||||
|
"collectionSortName": "이름",
|
||||||
|
|
||||||
|
"collectionGroupTitle": "묶음",
|
||||||
|
"collectionGroupAlbum": "앨범별로",
|
||||||
|
"collectionGroupMonth": "월별로",
|
||||||
|
"collectionGroupDay": "날짜별로",
|
||||||
|
"collectionGroupNone": "묶음 없음",
|
||||||
|
|
||||||
|
"sectionUnknown": "없음",
|
||||||
|
"dateToday": "오늘",
|
||||||
|
"dateYesterday": "어제",
|
||||||
|
"dateThisMonth": "이번 달",
|
||||||
|
"errorUnsupportedMimeType": "{mimeType} 지원되지 않음",
|
||||||
|
"collectionDeleteFailureFeedback": "{count, plural, other{항목 {count}개를 삭제하지 못했습니다}}",
|
||||||
|
"collectionCopyFailureFeedback": "{count, plural, other{항목 {count}개를 복사하지 못했습니다}}",
|
||||||
|
"collectionMoveFailureFeedback": "{count, plural, other{항목 {count}개를 이동하지 못했습니다}}",
|
||||||
|
"collectionExportFailureFeedback": "{count, plural, other{항목 {count}개를 내보내지 못했습니다}}",
|
||||||
|
"collectionCopySuccessFeedback": "{count, plural, other{항목 {count}개를 복사했습니다}}",
|
||||||
|
"collectionMoveSuccessFeedback": "{count, plural, other{항목 {count}개를 이동했습니다}}",
|
||||||
|
|
||||||
|
"collectionEmptyFavourites": "즐겨찾기가 없습니다",
|
||||||
|
"collectionEmptyVideos": "동영상이 없습니다",
|
||||||
|
"collectionEmptyImages": "사진이 없습니다",
|
||||||
|
|
||||||
|
"collectionSelectSectionTooltip": "묶음 선택",
|
||||||
|
"collectionDeselectSectionTooltip": "묶음 선택 해제",
|
||||||
|
|
||||||
|
"drawerCollectionAll": "모든 미디어",
|
||||||
|
"drawerCollectionVideos": "동영상",
|
||||||
|
"drawerCollectionFavourites": "즐겨찾기",
|
||||||
|
|
||||||
|
"chipSortTitle": "정렬",
|
||||||
|
"chipSortDate": "날짜",
|
||||||
|
"chipSortName": "이름",
|
||||||
|
"chipSortCount": "항목수",
|
||||||
|
|
||||||
|
"albumGroupTitle": "묶음",
|
||||||
|
"albumGroupTier": "단계별로",
|
||||||
|
"albumGroupVolume": "저장공간별로",
|
||||||
|
"albumGroupNone": "묶음 없음",
|
||||||
|
|
||||||
|
"albumPickPageTitleCopy": "앨범으로 복사",
|
||||||
|
"albumPickPageTitleExport": "앨범으로 내보내기",
|
||||||
|
"albumPickPageTitleMove": "앨범으로 이동",
|
||||||
|
|
||||||
|
"albumPageTitle": "앨범",
|
||||||
|
"albumEmpty": "앨범이 없습니다",
|
||||||
|
"createAlbumTooltip": "새 앨범 만들기",
|
||||||
|
"createAlbumButtonLabel": "추가",
|
||||||
|
|
||||||
|
"countryPageTitle": "국가",
|
||||||
|
"countryEmpty": "국가가 없습니다",
|
||||||
|
|
||||||
|
"tagPageTitle": "태그",
|
||||||
|
"tagEmpty": "태그가 없습니다",
|
||||||
|
|
||||||
|
"searchCollectionFieldHint": "미디어 검색",
|
||||||
|
"searchSectionRecent": "최근 검색기록",
|
||||||
|
"searchSectionAlbums": "앨범",
|
||||||
|
"searchSectionCountries": "국가",
|
||||||
|
"searchSectionPlaces": "장소",
|
||||||
|
"searchSectionTags": "태그",
|
||||||
|
|
||||||
|
"settingsPageTitle": "설정",
|
||||||
|
"settingsSystemDefault": "시스템",
|
||||||
|
|
||||||
|
"settingsSectionNavigation": "탐색",
|
||||||
|
"settingsHome": "홈",
|
||||||
|
"settingsDoubleBackExit": "뒤로가기 두번 눌러 앱 종료하기",
|
||||||
|
|
||||||
|
"settingsSectionDisplay": "디스플레이",
|
||||||
|
"settingsLanguage": "언어",
|
||||||
|
"settingsKeepScreenOnTile": "화면 자동 꺼짐 방지",
|
||||||
|
"settingsKeepScreenOnTitle": "화면 자동 꺼짐 방지",
|
||||||
|
"settingsRasterImageBackground": "래스터 그래픽스 배경",
|
||||||
|
"settingsVectorImageBackground": "벡터 그래픽스 배경",
|
||||||
|
"settingsCoordinateFormatTile": "좌표 표현",
|
||||||
|
"settingsCoordinateFormatTitle": "좌표 표현",
|
||||||
|
|
||||||
|
"settingsSectionThumbnails": "섬네일",
|
||||||
|
"settingsThumbnailShowLocationIcon": "위치 아이콘 표시",
|
||||||
|
"settingsThumbnailShowRawIcon": "Raw 아이콘 표시",
|
||||||
|
"settingsThumbnailShowVideoDuration": "동영상 길이 표시",
|
||||||
|
|
||||||
|
"settingsSectionViewer": "뷰어",
|
||||||
|
"settingsViewerShowMinimap": "미니맵 표시",
|
||||||
|
"settingsViewerShowInformation": "상세 정보 표시",
|
||||||
|
"settingsViewerShowInformationSubtitle": "제목, 날짜, 장소 등 표시",
|
||||||
|
"settingsViewerShowShootingDetails": "촬영 정보 표시",
|
||||||
|
|
||||||
|
"settingsSectionSearch": "검색",
|
||||||
|
"settingsSaveSearchHistory": "검색기록",
|
||||||
|
|
||||||
|
"settingsSectionPrivacy": "개인정보 보호",
|
||||||
|
"settingsEnableAnalytics": "진단 데이터 보내기",
|
||||||
|
|
||||||
|
"settingsHiddenFiltersTile": "숨겨진 필터",
|
||||||
|
"settingsHiddenFiltersTitle": "숨겨진 필터",
|
||||||
|
"settingsHiddenFiltersBanner": "이 필터에 맞는 사진과 동영상이 숨겨지고 있으며 이 앱에서 보여지지 않을 것입니다.",
|
||||||
|
"settingsHiddenFiltersEmpty": "숨겨진 필터가 없습니다",
|
||||||
|
|
||||||
|
"settingsStorageAccessTile": "저장공간 접근",
|
||||||
|
"settingsStorageAccessTitle": "저장공간 접근",
|
||||||
|
"settingsStorageAccessBanner": "어떤 폴더는 사용자의 허용을 받아야만 앱이 파일에 접근이 가능합니다. 이 화면에 허용을 받은 폴더를 확인할 수 있으며 원하지 않으면 취소할 수 있습니다.",
|
||||||
|
"settingsStorageAccessEmpty": "접근 허용이 없습니다",
|
||||||
|
"settingsStorageAccessRevokeTooltip": "취소",
|
||||||
|
|
||||||
|
"statsPageTitle": "통계",
|
||||||
|
"statsImage": "{count, plural, other{사진}}",
|
||||||
|
"statsVideo": "{count, plural, other{동영상}}",
|
||||||
|
"statsWithGps": "{count, plural, other{{count}개 위치가 있음}}",
|
||||||
|
"statsTopCountries": "국가 랭킹",
|
||||||
|
"statsTopPlaces": "장소 랭킹",
|
||||||
|
"statsTopTags": "태그 랭킹",
|
||||||
|
|
||||||
|
"viewerOpenPanoramaButtonLabel": "파노라마 열기",
|
||||||
|
"viewerOpenTooltip": "열기",
|
||||||
|
"viewerPauseTooltip": "일시정지",
|
||||||
|
"viewerPlayTooltip": "재생",
|
||||||
|
"viewerErrorUnknown": "아이구!",
|
||||||
|
"viewerErrorDoesNotExist": "파일이 존재하지 않습니다.",
|
||||||
|
|
||||||
|
"viewerInfoPageTitle": "상세정보",
|
||||||
|
"viewerInfoBackToViewerTooltip": "뷰어로",
|
||||||
|
|
||||||
|
"viewerInfoUnknown": "알 수 없음",
|
||||||
|
"viewerInfoLabelTitle": "제목",
|
||||||
|
"viewerInfoLabelDate": "날짜",
|
||||||
|
"viewerInfoLabelResolution": "해상도",
|
||||||
|
"viewerInfoLabelSize": "크기",
|
||||||
|
"viewerInfoLabelUri": "URI",
|
||||||
|
"viewerInfoLabelPath": "경로",
|
||||||
|
"viewerInfoLabelDuration": "길이",
|
||||||
|
"viewerInfoLabelOwner": "패키지",
|
||||||
|
"viewerInfoLabelCoordinates": "좌표",
|
||||||
|
"viewerInfoLabelAddress": "주소",
|
||||||
|
|
||||||
|
"viewerInfoMapStyleTitle": "지도 유형",
|
||||||
|
"viewerInfoMapStyleTooltip": "지도 유형 선택",
|
||||||
|
"viewerInfoMapZoomInTooltip": "확대",
|
||||||
|
"viewerInfoMapZoomOutTooltip": "축소",
|
||||||
|
"mapAttributionOsmHot": "지도 데이터 © [OpenStreetMap](https://www.openstreetmap.org/copyright) 기여자 • 타일 [HOT](https://www.hotosm.org/) • 호스팅 [OSM France](https://openstreetmap.fr/)",
|
||||||
|
"mapAttributionStamen": "지도 데이터 © [OpenStreetMap](https://www.openstreetmap.org/copyright) 기여자 • 타일 [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
|
||||||
|
|
||||||
|
"viewerInfoOpenEmbeddedFailureFeedback": "첨부 데이터 추출 오류",
|
||||||
|
"viewerInfoOpenLinkText": "열기",
|
||||||
|
"viewerInfoViewXmlLinkText": "XML 보기",
|
||||||
|
|
||||||
|
"viewerInfoSearchFieldLabel": "메타데이터 검색",
|
||||||
|
"viewerInfoSearchEmpty": "결과가 없습니다",
|
||||||
|
"viewerInfoSearchSuggestionDate": "날짜 및 시간",
|
||||||
|
"viewerInfoSearchSuggestionDescription": "설명",
|
||||||
|
"viewerInfoSearchSuggestionDimensions": "크기",
|
||||||
|
"viewerInfoSearchSuggestionResolution": "해상도",
|
||||||
|
"viewerInfoSearchSuggestionRights": "권리",
|
||||||
|
|
||||||
|
"panoramaEnableSensorControl": "센서 제어 활성화",
|
||||||
|
"panoramaDisableSensorControl": "센서 제어 비활성화",
|
||||||
|
|
||||||
|
"sourceViewerPageTitle": "소스 코드"
|
||||||
|
}
|
108
lib/main.dart
|
@ -1,14 +1,18 @@
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/media_store_source.dart';
|
import 'package:aves/model/source/media_store_source.dart';
|
||||||
|
import 'package:aves/services/services.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/theme/themes.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
import 'package:aves/utils/debouncer.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/providers/highlight_info_provider.dart';
|
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||||
import 'package:aves/widgets/home_page.dart';
|
import 'package:aves/widgets/home_page.dart';
|
||||||
import 'package:aves/widgets/welcome_page.dart';
|
import 'package:aves/widgets/welcome_page.dart';
|
||||||
|
@ -19,6 +23,8 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||||
import 'package:overlay_support/overlay_support.dart';
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -37,16 +43,13 @@ void main() {
|
||||||
runApp(AvesApp());
|
runApp(AvesApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AppMode { main, pick, view }
|
|
||||||
|
|
||||||
class AvesApp extends StatefulWidget {
|
class AvesApp extends StatefulWidget {
|
||||||
static AppMode mode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_AvesAppState createState() => _AvesAppState();
|
_AvesAppState createState() => _AvesAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AvesAppState extends State<AvesApp> {
|
class _AvesAppState extends State<AvesApp> {
|
||||||
|
final ValueNotifier<AppMode> appModeNotifier = ValueNotifier(AppMode.main);
|
||||||
Future<void> _appSetup;
|
Future<void> _appSetup;
|
||||||
final _mediaStoreSource = MediaStoreSource();
|
final _mediaStoreSource = MediaStoreSource();
|
||||||
final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay);
|
final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay);
|
||||||
|
@ -59,49 +62,12 @@ class _AvesAppState extends State<AvesApp> {
|
||||||
final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
|
final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
|
||||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||||
|
|
||||||
static const accentColor = Colors.indigoAccent;
|
|
||||||
|
|
||||||
static final darkTheme = ThemeData(
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
accentColor: accentColor,
|
|
||||||
scaffoldBackgroundColor: Colors.grey[900],
|
|
||||||
buttonColor: accentColor,
|
|
||||||
dialogBackgroundColor: Colors.grey[850],
|
|
||||||
toggleableActiveColor: accentColor,
|
|
||||||
tooltipTheme: TooltipThemeData(
|
|
||||||
verticalOffset: 32,
|
|
||||||
),
|
|
||||||
appBarTheme: AppBarTheme(
|
|
||||||
textTheme: TextTheme(
|
|
||||||
headline6: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
fontFeatures: [FontFeature.enable('smcp')],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
primary: accentColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
primary: accentColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
textButtonTheme: TextButtonThemeData(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
primary: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget getFirstPage({Map intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : WelcomePage();
|
Widget getFirstPage({Map intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : WelcomePage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
initPlatformServices();
|
||||||
_appSetup = _setup();
|
_appSetup = _setup();
|
||||||
_contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String));
|
_contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String));
|
||||||
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map));
|
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map));
|
||||||
|
@ -113,27 +79,41 @@ class _AvesAppState extends State<AvesApp> {
|
||||||
// so it can be used during navigation transitions
|
// so it can be used during navigation transitions
|
||||||
return ChangeNotifierProvider<Settings>.value(
|
return ChangeNotifierProvider<Settings>.value(
|
||||||
value: settings,
|
value: settings,
|
||||||
child: Provider<CollectionSource>.value(
|
child: ListenableProvider<ValueNotifier<AppMode>>.value(
|
||||||
value: _mediaStoreSource,
|
value: appModeNotifier,
|
||||||
child: HighlightInfoProvider(
|
child: Provider<CollectionSource>.value(
|
||||||
child: OverlaySupport(
|
value: _mediaStoreSource,
|
||||||
child: FutureBuilder<void>(
|
child: HighlightInfoProvider(
|
||||||
future: _appSetup,
|
child: OverlaySupport(
|
||||||
builder: (context, snapshot) {
|
child: FutureBuilder<void>(
|
||||||
final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done)
|
future: _appSetup,
|
||||||
? getFirstPage()
|
builder: (context, snapshot) {
|
||||||
: Scaffold(
|
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
|
||||||
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(),
|
final home = initialized
|
||||||
);
|
? getFirstPage()
|
||||||
return MaterialApp(
|
: Scaffold(
|
||||||
navigatorKey: _navigatorKey,
|
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(),
|
||||||
home: home,
|
);
|
||||||
navigatorObservers: _navigatorObservers,
|
return Selector<Settings, Locale>(
|
||||||
title: 'Aves',
|
selector: (context, s) => s.locale,
|
||||||
darkTheme: darkTheme,
|
builder: (context, settingsLocale, child) {
|
||||||
themeMode: ThemeMode.dark,
|
return MaterialApp(
|
||||||
);
|
navigatorKey: _navigatorKey,
|
||||||
},
|
home: home,
|
||||||
|
navigatorObservers: _navigatorObservers,
|
||||||
|
onGenerateTitle: (context) => context.l10n.appName,
|
||||||
|
darkTheme: Themes.darkTheme,
|
||||||
|
themeMode: ThemeMode.dark,
|
||||||
|
locale: settingsLocale,
|
||||||
|
localizationsDelegates: [
|
||||||
|
...AppLocalizations.localizationsDelegates,
|
||||||
|
LocaleNamesLocalizationsDelegate(),
|
||||||
|
],
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -183,7 +163,7 @@ class _AvesAppState extends State<AvesApp> {
|
||||||
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 (AvesApp.mode == AppMode.main && (intentData == null || intentData.isEmpty == true)) return;
|
if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return;
|
||||||
|
|
||||||
FirebaseCrashlytics.instance.log('New intent');
|
FirebaseCrashlytics.instance.log('New intent');
|
||||||
_navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute(
|
_navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute(
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum ChipSetAction {
|
enum ChipSetAction {
|
||||||
group,
|
group,
|
||||||
sort,
|
sort,
|
||||||
refresh,
|
|
||||||
stats,
|
stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,30 +14,33 @@ enum ChipAction {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
rename,
|
rename,
|
||||||
|
setCover,
|
||||||
goToAlbumPage,
|
goToAlbumPage,
|
||||||
goToCountryPage,
|
goToCountryPage,
|
||||||
goToTagPage,
|
goToTagPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExtraChipAction on ChipAction {
|
extension ExtraChipAction on ChipAction {
|
||||||
String getText() {
|
String getText(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ChipAction.delete:
|
case ChipAction.delete:
|
||||||
return 'Delete';
|
return context.l10n.chipActionDelete;
|
||||||
case ChipAction.goToAlbumPage:
|
case ChipAction.goToAlbumPage:
|
||||||
return 'Show in Albums';
|
return context.l10n.chipActionGoToAlbumPage;
|
||||||
case ChipAction.goToCountryPage:
|
case ChipAction.goToCountryPage:
|
||||||
return 'Show in Countries';
|
return context.l10n.chipActionGoToCountryPage;
|
||||||
case ChipAction.goToTagPage:
|
case ChipAction.goToTagPage:
|
||||||
return 'Show in Tags';
|
return context.l10n.chipActionGoToTagPage;
|
||||||
case ChipAction.hide:
|
case ChipAction.hide:
|
||||||
return 'Hide';
|
return context.l10n.chipActionHide;
|
||||||
case ChipAction.pin:
|
case ChipAction.pin:
|
||||||
return 'Pin to top';
|
return context.l10n.chipActionPin;
|
||||||
case ChipAction.unpin:
|
case ChipAction.unpin:
|
||||||
return 'Unpin from top';
|
return context.l10n.chipActionUnpin;
|
||||||
case ChipAction.rename:
|
case ChipAction.rename:
|
||||||
return 'Rename';
|
return context.l10n.chipActionRename;
|
||||||
|
case ChipAction.setCover:
|
||||||
|
return context.l10n.chipActionSetCover;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +62,8 @@ extension ExtraChipAction on ChipAction {
|
||||||
return AIcons.pin;
|
return AIcons.pin;
|
||||||
case ChipAction.rename:
|
case ChipAction.rename:
|
||||||
return AIcons.rename;
|
return AIcons.rename;
|
||||||
|
case ChipAction.setCover:
|
||||||
|
return AIcons.setCover;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ enum CollectionAction {
|
||||||
addShortcut,
|
addShortcut,
|
||||||
sort,
|
sort,
|
||||||
group,
|
group,
|
||||||
refresh,
|
|
||||||
select,
|
select,
|
||||||
selectAll,
|
selectAll,
|
||||||
selectNone,
|
selectNone,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum EntryAction {
|
enum EntryAction {
|
||||||
|
@ -46,41 +47,41 @@ class EntryActions {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExtraEntryAction on EntryAction {
|
extension ExtraEntryAction on EntryAction {
|
||||||
String getText() {
|
String getText(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
// in app actions
|
// in app actions
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
// different data depending on toggle state
|
// different data depending on toggle state
|
||||||
return null;
|
return null;
|
||||||
case EntryAction.delete:
|
case EntryAction.delete:
|
||||||
return 'Delete';
|
return context.l10n.entryActionDelete;
|
||||||
case EntryAction.export:
|
case EntryAction.export:
|
||||||
return 'Export';
|
return context.l10n.entryActionExport;
|
||||||
case EntryAction.info:
|
case EntryAction.info:
|
||||||
return 'Info';
|
return context.l10n.entryActionInfo;
|
||||||
case EntryAction.rename:
|
case EntryAction.rename:
|
||||||
return 'Rename';
|
return context.l10n.entryActionRename;
|
||||||
case EntryAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
return 'Rotate counterclockwise';
|
return context.l10n.entryActionRotateCCW;
|
||||||
case EntryAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
return 'Rotate clockwise';
|
return context.l10n.entryActionRotateCW;
|
||||||
case EntryAction.flip:
|
case EntryAction.flip:
|
||||||
return 'Flip horizontally';
|
return context.l10n.entryActionFlip;
|
||||||
case EntryAction.print:
|
case EntryAction.print:
|
||||||
return 'Print';
|
return context.l10n.entryActionPrint;
|
||||||
case EntryAction.share:
|
case EntryAction.share:
|
||||||
return 'Share';
|
return context.l10n.entryActionShare;
|
||||||
case EntryAction.viewSource:
|
case EntryAction.viewSource:
|
||||||
return 'View source';
|
return context.l10n.entryActionViewSource;
|
||||||
// external app actions
|
// external app actions
|
||||||
case EntryAction.edit:
|
case EntryAction.edit:
|
||||||
return 'Edit with…';
|
return context.l10n.entryActionEdit;
|
||||||
case EntryAction.open:
|
case EntryAction.open:
|
||||||
return 'Open with…';
|
return context.l10n.entryActionOpen;
|
||||||
case EntryAction.setAs:
|
case EntryAction.setAs:
|
||||||
return 'Set as…';
|
return context.l10n.entryActionSetAs;
|
||||||
case EntryAction.openMap:
|
case EntryAction.openMap:
|
||||||
return 'Show on map…';
|
return context.l10n.entryActionOpenMap;
|
||||||
case EntryAction.debug:
|
case EntryAction.debug:
|
||||||
return 'Debug';
|
return 'Debug';
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,29 @@ import 'package:google_api_availability/google_api_availability.dart';
|
||||||
import 'package:package_info/package_info.dart';
|
import 'package:package_info/package_info.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
final AvesAvailability availability = AvesAvailability._private();
|
abstract class AvesAvailability {
|
||||||
|
void onResume();
|
||||||
|
|
||||||
class AvesAvailability {
|
Future<bool> get isConnected;
|
||||||
|
|
||||||
|
Future<bool> get hasPlayServices;
|
||||||
|
|
||||||
|
Future<bool> get canLocatePlaces;
|
||||||
|
|
||||||
|
Future<bool> get isNewVersionAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveAvesAvailability implements AvesAvailability {
|
||||||
bool _isConnected, _hasPlayServices, _isNewVersionAvailable;
|
bool _isConnected, _hasPlayServices, _isNewVersionAvailable;
|
||||||
|
|
||||||
AvesAvailability._private() {
|
LiveAvesAvailability() {
|
||||||
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
|
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
void onResume() => _isConnected = null;
|
void onResume() => _isConnected = null;
|
||||||
|
|
||||||
|
@override
|
||||||
Future<bool> get isConnected async {
|
Future<bool> get isConnected async {
|
||||||
if (_isConnected != null) return SynchronousFuture(_isConnected);
|
if (_isConnected != null) return SynchronousFuture(_isConnected);
|
||||||
final result = await (Connectivity().checkConnectivity());
|
final result = await (Connectivity().checkConnectivity());
|
||||||
|
@ -34,6 +46,7 @@ class AvesAvailability {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<bool> get hasPlayServices async {
|
Future<bool> get hasPlayServices async {
|
||||||
if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices);
|
if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices);
|
||||||
final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability();
|
final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability();
|
||||||
|
@ -43,8 +56,10 @@ class AvesAvailability {
|
||||||
}
|
}
|
||||||
|
|
||||||
// local geocoding with `geocoder` requires Play Services
|
// local geocoding with `geocoder` requires Play Services
|
||||||
|
@override
|
||||||
Future<bool> get canLocatePlaces => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result));
|
Future<bool> get canLocatePlaces => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result));
|
||||||
|
|
||||||
|
@override
|
||||||
Future<bool> get isNewVersionAvailable async {
|
Future<bool> get isNewVersionAvailable async {
|
||||||
if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable);
|
if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable);
|
||||||
|
|
||||||
|
|
111
lib/model/covers.dart
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/filters/album.dart';
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/services/services.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
final Covers covers = Covers._private();
|
||||||
|
|
||||||
|
class Covers with ChangeNotifier {
|
||||||
|
Set<CoverRow> _rows = {};
|
||||||
|
|
||||||
|
Covers._private();
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
_rows = await metadataDb.loadCovers();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get count => _rows.length;
|
||||||
|
|
||||||
|
int coverContentId(CollectionFilter filter) => _rows.firstWhere((row) => row.filter == filter, orElse: () => null)?.contentId;
|
||||||
|
|
||||||
|
Future<void> set(CollectionFilter filter, int contentId) async {
|
||||||
|
// erase contextual properties from filters before saving them
|
||||||
|
if (filter is AlbumFilter) {
|
||||||
|
filter = AlbumFilter((filter as AlbumFilter).album, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final row = CoverRow(filter: filter, contentId: contentId);
|
||||||
|
_rows.removeWhere((row) => row.filter == filter);
|
||||||
|
if (contentId == null) {
|
||||||
|
await metadataDb.removeCovers({row});
|
||||||
|
} else {
|
||||||
|
_rows.add(row);
|
||||||
|
await metadataDb.addCovers({row});
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> moveEntry(int oldContentId, AvesEntry entry) async {
|
||||||
|
final oldRows = _rows.where((row) => row.contentId == oldContentId).toSet();
|
||||||
|
if (oldRows.isEmpty) return;
|
||||||
|
|
||||||
|
for (final oldRow in oldRows) {
|
||||||
|
final filter = oldRow.filter;
|
||||||
|
_rows.remove(oldRow);
|
||||||
|
if (filter.test(entry)) {
|
||||||
|
final newRow = CoverRow(filter: filter, contentId: entry.contentId);
|
||||||
|
await metadataDb.updateCoverEntryId(oldRow.contentId, newRow);
|
||||||
|
_rows.add(newRow);
|
||||||
|
} else {
|
||||||
|
await metadataDb.removeCovers({oldRow});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeEntries(Set<AvesEntry> entries) async {
|
||||||
|
final contentIds = entries.map((entry) => entry.contentId).toSet();
|
||||||
|
final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet();
|
||||||
|
|
||||||
|
await metadataDb.removeCovers(removedRows);
|
||||||
|
_rows.removeAll(removedRows);
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
await metadataDb.clearCovers();
|
||||||
|
_rows.clear();
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class CoverRow {
|
||||||
|
final CollectionFilter filter;
|
||||||
|
final int contentId;
|
||||||
|
|
||||||
|
const CoverRow({
|
||||||
|
@required this.filter,
|
||||||
|
@required this.contentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CoverRow.fromMap(Map map) {
|
||||||
|
return CoverRow(
|
||||||
|
filter: CollectionFilter.fromJson(map['filter']),
|
||||||
|
contentId: map['contentId'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'filter': filter.toJson(),
|
||||||
|
'contentId': contentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is CoverRow && other.filter == filter && other.contentId == contentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(filter, contentId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType#${shortHash(this)}{filter=$filter, contentId=$contentId}';
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/geo/countries.dart';
|
import 'package:aves/geo/countries.dart';
|
||||||
import 'package:aves/model/availability.dart';
|
|
||||||
import 'package:aves/model/entry_cache.dart';
|
import 'package:aves/model/entry_cache.dart';
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/metadata.dart';
|
import 'package:aves/model/metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
|
||||||
import 'package:aves/model/multipage.dart';
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/geocoding_service.dart';
|
||||||
import 'package:aves/services/service_policy.dart';
|
import 'package:aves/services/service_policy.dart';
|
||||||
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:aves/services/svg_metadata_service.dart';
|
import 'package:aves/services/svg_metadata_service.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
|
@ -18,7 +17,6 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:country_code/country_code.dart';
|
import 'package:country_code/country_code.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geocoder/geocoder.dart';
|
|
||||||
import 'package:latlong/latlong.dart';
|
import 'package:latlong/latlong.dart';
|
||||||
import 'package:path/path.dart' as ppath;
|
import 'package:path/path.dart' as ppath;
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ class AvesEntry {
|
||||||
int height;
|
int height;
|
||||||
int sourceRotationDegrees;
|
int sourceRotationDegrees;
|
||||||
final int sizeBytes;
|
final int sizeBytes;
|
||||||
String sourceTitle;
|
String _sourceTitle;
|
||||||
|
|
||||||
// `dateModifiedSecs` can be missing in viewer mode
|
// `dateModifiedSecs` can be missing in viewer mode
|
||||||
int _dateModifiedSecs;
|
int _dateModifiedSecs;
|
||||||
|
@ -45,10 +43,6 @@ class AvesEntry {
|
||||||
|
|
||||||
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
||||||
|
|
||||||
// Local geocoding requires Google Play Services
|
|
||||||
// Google remote geocoding requires an API key and is not free
|
|
||||||
final Future<List<Address>> Function(Coordinates coordinates) _findAddresses = Geocoder.local.findAddressesFromCoordinates;
|
|
||||||
|
|
||||||
// TODO TLAD make it dynamic if it depends on OS/lib versions
|
// TODO TLAD make it dynamic if it depends on OS/lib versions
|
||||||
static const List<String> undecodable = [MimeTypes.crw, MimeTypes.djvu, MimeTypes.psd];
|
static const List<String> undecodable = [MimeTypes.crw, MimeTypes.djvu, MimeTypes.psd];
|
||||||
|
|
||||||
|
@ -62,13 +56,14 @@ class AvesEntry {
|
||||||
@required this.height,
|
@required this.height,
|
||||||
this.sourceRotationDegrees,
|
this.sourceRotationDegrees,
|
||||||
this.sizeBytes,
|
this.sizeBytes,
|
||||||
this.sourceTitle,
|
String sourceTitle,
|
||||||
int dateModifiedSecs,
|
int dateModifiedSecs,
|
||||||
this.sourceDateTakenMillis,
|
this.sourceDateTakenMillis,
|
||||||
this.durationMillis,
|
this.durationMillis,
|
||||||
}) : assert(width != null),
|
}) : assert(width != null),
|
||||||
assert(height != null) {
|
assert(height != null) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
this.sourceTitle = sourceTitle;
|
||||||
this.dateModifiedSecs = dateModifiedSecs;
|
this.dateModifiedSecs = dateModifiedSecs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,14 +72,14 @@ class AvesEntry {
|
||||||
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
|
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
|
||||||
|
|
||||||
AvesEntry copyWith({
|
AvesEntry copyWith({
|
||||||
@required String uri,
|
String uri,
|
||||||
@required String path,
|
String path,
|
||||||
@required int contentId,
|
int contentId,
|
||||||
@required int dateModifiedSecs,
|
int dateModifiedSecs,
|
||||||
}) {
|
}) {
|
||||||
final copyContentId = contentId ?? this.contentId;
|
final copyContentId = contentId ?? this.contentId;
|
||||||
final copied = AvesEntry(
|
final copied = AvesEntry(
|
||||||
uri: uri ?? uri,
|
uri: uri ?? this.uri,
|
||||||
path: path ?? this.path,
|
path: path ?? this.path,
|
||||||
contentId: copyContentId,
|
contentId: copyContentId,
|
||||||
sourceMimeType: sourceMimeType,
|
sourceMimeType: sourceMimeType,
|
||||||
|
@ -93,7 +88,7 @@ class AvesEntry {
|
||||||
sourceRotationDegrees: sourceRotationDegrees,
|
sourceRotationDegrees: sourceRotationDegrees,
|
||||||
sizeBytes: sizeBytes,
|
sizeBytes: sizeBytes,
|
||||||
sourceTitle: sourceTitle,
|
sourceTitle: sourceTitle,
|
||||||
dateModifiedSecs: dateModifiedSecs,
|
dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs,
|
||||||
sourceDateTakenMillis: sourceDateTakenMillis,
|
sourceDateTakenMillis: sourceDateTakenMillis,
|
||||||
durationMillis: durationMillis,
|
durationMillis: durationMillis,
|
||||||
)
|
)
|
||||||
|
@ -241,9 +236,7 @@ class AvesEntry {
|
||||||
|
|
||||||
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||||
|
|
||||||
// as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved
|
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
|
||||||
// so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution
|
|
||||||
bool get useTiles => supportTiling && (width > 4096 || height > 4096 || is360);
|
|
||||||
|
|
||||||
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
||||||
|
|
||||||
|
@ -347,6 +340,13 @@ class AvesEntry {
|
||||||
|
|
||||||
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
|
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
|
||||||
|
|
||||||
|
String get sourceTitle => _sourceTitle;
|
||||||
|
|
||||||
|
set sourceTitle(String sourceTitle) {
|
||||||
|
_sourceTitle = sourceTitle;
|
||||||
|
_bestTitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
int get dateModifiedSecs => _dateModifiedSecs;
|
int get dateModifiedSecs => _dateModifiedSecs;
|
||||||
|
|
||||||
set dateModifiedSecs(int dateModifiedSecs) {
|
set dateModifiedSecs(int dateModifiedSecs) {
|
||||||
|
@ -444,7 +444,7 @@ class AvesEntry {
|
||||||
}
|
}
|
||||||
catalogMetadata = CatalogMetadata(contentId: contentId);
|
catalogMetadata = CatalogMetadata(contentId: contentId);
|
||||||
} else {
|
} else {
|
||||||
catalogMetadata = await MetadataService.getCatalogMetadata(this, background: background);
|
catalogMetadata = await metadataService.getCatalogMetadata(this, background: background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,12 +479,18 @@ class AvesEntry {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _geocoderLocale;
|
||||||
|
|
||||||
|
String get geocoderLocale {
|
||||||
|
_geocoderLocale ??= (settings.locale ?? WidgetsBinding.instance.window.locale).toString();
|
||||||
|
return _geocoderLocale;
|
||||||
|
}
|
||||||
|
|
||||||
// full reverse geocoding, requiring Play Services and some connectivity
|
// full reverse geocoding, requiring Play Services and some connectivity
|
||||||
Future<void> locatePlace({@required bool background}) async {
|
Future<void> locatePlace({@required bool background}) async {
|
||||||
if (!hasGps || hasFineAddress) return;
|
if (!hasGps || hasFineAddress) return;
|
||||||
final coordinates = latLng;
|
|
||||||
try {
|
try {
|
||||||
Future<List<Address>> call() => _findAddresses(Coordinates(coordinates.latitude, coordinates.longitude));
|
Future<List<Address>> call() => GeocodingService.getAddress(latLng, geocoderLocale);
|
||||||
final addresses = await (background
|
final addresses = await (background
|
||||||
? servicePolicy.call(
|
? servicePolicy.call(
|
||||||
call,
|
call,
|
||||||
|
@ -507,22 +513,21 @@ class AvesEntry {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
debugPrint('$runtimeType locate failed with path=$path coordinates=$coordinates error=$error\n$stack');
|
debugPrint('$runtimeType locate failed with path=$path coordinates=$latLng error=$error\n$stack');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> findAddressLine() async {
|
Future<String> findAddressLine() async {
|
||||||
if (!hasGps) return null;
|
if (!hasGps) return null;
|
||||||
|
|
||||||
final coordinates = latLng;
|
|
||||||
try {
|
try {
|
||||||
final addresses = await _findAddresses(Coordinates(coordinates.latitude, coordinates.longitude));
|
final addresses = await GeocodingService.getAddress(latLng, geocoderLocale);
|
||||||
if (addresses != null && addresses.isNotEmpty) {
|
if (addresses != null && addresses.isNotEmpty) {
|
||||||
final address = addresses.first;
|
final address = addresses.first;
|
||||||
return address.addressLine;
|
return address.addressLine;
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$coordinates error=$error\n$stack');
|
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$latLng error=$error\n$stack');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -553,10 +558,7 @@ class AvesEntry {
|
||||||
final contentId = newFields['contentId'];
|
final contentId = newFields['contentId'];
|
||||||
if (contentId is int) this.contentId = contentId;
|
if (contentId is int) this.contentId = contentId;
|
||||||
final sourceTitle = newFields['title'];
|
final sourceTitle = newFields['title'];
|
||||||
if (sourceTitle is String) {
|
if (sourceTitle is String) this.sourceTitle = sourceTitle;
|
||||||
this.sourceTitle = sourceTitle;
|
|
||||||
_bestTitle = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final width = newFields['width'];
|
final width = newFields['width'];
|
||||||
if (width is int) this.width = width;
|
if (width is int) this.width = width;
|
||||||
|
@ -576,18 +578,8 @@ class AvesEntry {
|
||||||
metadataChangeNotifier.notifyListeners();
|
metadataChangeNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> rename(String newName) async {
|
|
||||||
if (newName == filenameWithoutExtension) return true;
|
|
||||||
|
|
||||||
final newFields = await ImageFileService.rename(this, '$newName$extension');
|
|
||||||
if (newFields.isEmpty) return false;
|
|
||||||
|
|
||||||
await _applyNewFields(newFields);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> rotate({@required bool clockwise}) async {
|
Future<bool> rotate({@required bool clockwise}) async {
|
||||||
final newFields = await ImageFileService.rotate(this, clockwise: clockwise);
|
final newFields = await imageFileService.rotate(this, clockwise: clockwise);
|
||||||
if (newFields.isEmpty) return false;
|
if (newFields.isEmpty) return false;
|
||||||
|
|
||||||
final oldDateModifiedSecs = dateModifiedSecs;
|
final oldDateModifiedSecs = dateModifiedSecs;
|
||||||
|
@ -599,7 +591,7 @@ class AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> flip() async {
|
Future<bool> flip() async {
|
||||||
final newFields = await ImageFileService.flip(this);
|
final newFields = await imageFileService.flip(this);
|
||||||
if (newFields.isEmpty) return false;
|
if (newFields.isEmpty) return false;
|
||||||
|
|
||||||
final oldDateModifiedSecs = dateModifiedSecs;
|
final oldDateModifiedSecs = dateModifiedSecs;
|
||||||
|
@ -612,7 +604,7 @@ class AvesEntry {
|
||||||
|
|
||||||
Future<bool> delete() {
|
Future<bool> delete() {
|
||||||
Completer completer = Completer<bool>();
|
Completer completer = Completer<bool>();
|
||||||
ImageFileService.delete([this]).listen(
|
imageFileService.delete([this]).listen(
|
||||||
(event) => completer.complete(event.success),
|
(event) => completer.complete(event.success),
|
||||||
onError: completer.completeError,
|
onError: completer.completeError,
|
||||||
onDone: () {
|
onDone: () {
|
||||||
|
@ -625,7 +617,7 @@ class AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the entry image itself changed (e.g. after rotation)
|
// when the entry image itself changed (e.g. after rotation)
|
||||||
void _onImageChanged(int oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async {
|
Future<void> _onImageChanged(int oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async {
|
||||||
if (oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
|
if (oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
|
||||||
await EntryCache.evict(uri, mimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
await EntryCache.evict(uri, mimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||||
imageChangeNotifier.notifyListeners();
|
imageChangeNotifier.notifyListeners();
|
||||||
|
@ -634,23 +626,23 @@ class AvesEntry {
|
||||||
|
|
||||||
// favourites
|
// favourites
|
||||||
|
|
||||||
void toggleFavourite() {
|
Future<void> toggleFavourite() async {
|
||||||
if (isFavourite) {
|
if (isFavourite) {
|
||||||
removeFromFavourites();
|
await removeFromFavourites();
|
||||||
} else {
|
} else {
|
||||||
addToFavourites();
|
await addToFavourites();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addToFavourites() {
|
Future<void> addToFavourites() async {
|
||||||
if (!isFavourite) {
|
if (!isFavourite) {
|
||||||
favourites.add([this]);
|
await favourites.add([this]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeFromFavourites() {
|
Future<void> removeFromFavourites() async {
|
||||||
if (isFavourite) {
|
if (isFavourite) {
|
||||||
favourites.remove([this]);
|
await favourites.remove([this]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/metadata.dart';
|
|
||||||
import 'package:aves/model/metadata_db.dart';
|
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
|
||||||
|
|
||||||
final FavouriteRepo favourites = FavouriteRepo._private();
|
|
||||||
|
|
||||||
class FavouriteRepo {
|
|
||||||
List<FavouriteRow> _rows = [];
|
|
||||||
|
|
||||||
final AChangeNotifier changeNotifier = AChangeNotifier();
|
|
||||||
|
|
||||||
FavouriteRepo._private();
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
_rows = await metadataDb.loadFavourites();
|
|
||||||
}
|
|
||||||
|
|
||||||
int get count => _rows.length;
|
|
||||||
|
|
||||||
bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId);
|
|
||||||
|
|
||||||
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path);
|
|
||||||
|
|
||||||
Future<void> add(Iterable<AvesEntry> entries) async {
|
|
||||||
final newRows = entries.map(_entryToRow);
|
|
||||||
|
|
||||||
await metadataDb.addFavourites(newRows);
|
|
||||||
_rows.addAll(newRows);
|
|
||||||
|
|
||||||
changeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> remove(Iterable<AvesEntry> entries) async {
|
|
||||||
final removedRows = entries.map(_entryToRow);
|
|
||||||
|
|
||||||
await metadataDb.removeFavourites(removedRows);
|
|
||||||
removedRows.forEach(_rows.remove);
|
|
||||||
|
|
||||||
changeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> move(int oldContentId, AvesEntry entry) async {
|
|
||||||
final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null);
|
|
||||||
final newRow = _entryToRow(entry);
|
|
||||||
|
|
||||||
await metadataDb.updateFavouriteId(oldContentId, newRow);
|
|
||||||
_rows.remove(oldRow);
|
|
||||||
_rows.add(newRow);
|
|
||||||
|
|
||||||
changeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clear() async {
|
|
||||||
await metadataDb.clearFavourites();
|
|
||||||
_rows.clear();
|
|
||||||
|
|
||||||
changeNotifier.notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
96
lib/model/favourites.dart
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/services/services.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
final Favourites favourites = Favourites._private();
|
||||||
|
|
||||||
|
class Favourites with ChangeNotifier {
|
||||||
|
Set<FavouriteRow> _rows = {};
|
||||||
|
|
||||||
|
Favourites._private();
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
_rows = await metadataDb.loadFavourites();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get count => _rows.length;
|
||||||
|
|
||||||
|
bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId);
|
||||||
|
|
||||||
|
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path);
|
||||||
|
|
||||||
|
Future<void> add(Iterable<AvesEntry> entries) async {
|
||||||
|
final newRows = entries.map(_entryToRow);
|
||||||
|
|
||||||
|
await metadataDb.addFavourites(newRows);
|
||||||
|
_rows.addAll(newRows);
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> remove(Iterable<AvesEntry> entries) async {
|
||||||
|
final contentIds = entries.map((entry) => entry.contentId).toSet();
|
||||||
|
final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet();
|
||||||
|
|
||||||
|
await metadataDb.removeFavourites(removedRows);
|
||||||
|
removedRows.forEach(_rows.remove);
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> moveEntry(int oldContentId, AvesEntry entry) async {
|
||||||
|
final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null);
|
||||||
|
if (oldRow == null) return;
|
||||||
|
|
||||||
|
final newRow = _entryToRow(entry);
|
||||||
|
|
||||||
|
await metadataDb.updateFavouriteId(oldContentId, newRow);
|
||||||
|
_rows.remove(oldRow);
|
||||||
|
_rows.add(newRow);
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
await metadataDb.clearFavourites();
|
||||||
|
_rows.clear();
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class FavouriteRow {
|
||||||
|
final int contentId;
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
const FavouriteRow({
|
||||||
|
this.contentId,
|
||||||
|
this.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FavouriteRow.fromMap(Map map) {
|
||||||
|
return FavouriteRow(
|
||||||
|
contentId: map['contentId'],
|
||||||
|
path: map['path'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'contentId': contentId,
|
||||||
|
'path': path,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is FavouriteRow && other.contentId == contentId && other.path == path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(contentId, path);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, path=$path}';
|
||||||
|
}
|
|
@ -35,10 +35,10 @@ class AlbumFilter extends CollectionFilter {
|
||||||
EntryFilter get test => (entry) => entry.directory == album;
|
EntryFilter get test => (entry) => entry.directory == album;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => uniqueName ?? album.split(separator).last;
|
String get universalLabel => uniqueName ?? album.split(separator).last;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tooltip => album;
|
String getTooltip(BuildContext context) => album;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
|
||||||
|
@ -74,7 +74,11 @@ class AlbumFilter extends CollectionFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
String get category => type;
|
||||||
|
|
||||||
|
// key `album-{path}` is expected by test driver
|
||||||
|
@override
|
||||||
|
String get key => '$type-$album';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class FavouriteFilter extends CollectionFilter {
|
class FavouriteFilter extends CollectionFilter {
|
||||||
static const type = 'favourite';
|
static const type = 'favourite';
|
||||||
|
|
||||||
|
const FavouriteFilter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
'type': type,
|
'type': type,
|
||||||
|
@ -15,13 +19,22 @@ class FavouriteFilter extends CollectionFilter {
|
||||||
EntryFilter get test => (entry) => entry.isFavourite;
|
EntryFilter get test => (entry) => entry.isFavourite;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => 'Favourite';
|
String get universalLabel => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size);
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
||||||
static const List<String> collectionFilterOrder = [
|
static const List<String> categoryOrder = [
|
||||||
QueryFilter.type,
|
QueryFilter.type,
|
||||||
FavouriteFilter.type,
|
FavouriteFilter.type,
|
||||||
MimeFilter.type,
|
MimeFilter.type,
|
||||||
|
@ -57,25 +57,28 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
||||||
|
|
||||||
bool get isUnique => true;
|
bool get isUnique => true;
|
||||||
|
|
||||||
String get label;
|
String get universalLabel;
|
||||||
|
|
||||||
String get tooltip => label;
|
String getLabel(BuildContext context) => universalLabel;
|
||||||
|
|
||||||
|
String getTooltip(BuildContext context) => getLabel(context);
|
||||||
|
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false});
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false});
|
||||||
|
|
||||||
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(label));
|
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context)));
|
||||||
|
|
||||||
String get typeKey;
|
String get category;
|
||||||
|
|
||||||
int get displayPriority => collectionFilterOrder.indexOf(typeKey);
|
|
||||||
|
|
||||||
// to be used as widget key
|
// to be used as widget key
|
||||||
String get key => '$typeKey-$label';
|
String get key;
|
||||||
|
|
||||||
|
int get displayPriority => categoryOrder.indexOf(category);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(CollectionFilter other) {
|
int compareTo(CollectionFilter other) {
|
||||||
final c = displayPriority.compareTo(other.displayPriority);
|
final c = displayPriority.compareTo(other.displayPriority);
|
||||||
return c != 0 ? c : compareAsciiUpperCase(label, other.label);
|
// assume we compare context-independent labels
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(universalLabel, other.universalLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class LocationFilter extends CollectionFilter {
|
class LocationFilter extends CollectionFilter {
|
||||||
static const type = 'location';
|
static const type = 'location';
|
||||||
static const emptyLabel = 'unlocated';
|
|
||||||
static const locationSeparator = ';';
|
static const locationSeparator = ';';
|
||||||
|
|
||||||
final LocationLevel level;
|
final LocationLevel level;
|
||||||
|
@ -48,7 +48,10 @@ class LocationFilter extends CollectionFilter {
|
||||||
EntryFilter get test => _test;
|
EntryFilter get test => _test;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => _location.isEmpty ? emptyLabel : _location;
|
String get universalLabel => _location;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
|
||||||
|
@ -66,7 +69,10 @@ class LocationFilter extends CollectionFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => '$type-$level-$_location';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/mime_utils.dart';
|
import 'package:aves/utils/mime_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -17,14 +19,12 @@ class MimeFilter extends CollectionFilter {
|
||||||
if (lowMime.endsWith('/*')) {
|
if (lowMime.endsWith('/*')) {
|
||||||
lowMime = lowMime.substring(0, lowMime.length - 2);
|
lowMime = lowMime.substring(0, lowMime.length - 2);
|
||||||
_test = (entry) => entry.mimeType.startsWith(lowMime);
|
_test = (entry) => entry.mimeType.startsWith(lowMime);
|
||||||
if (lowMime == 'video') {
|
_label = lowMime.toUpperCase();
|
||||||
_label = 'Video';
|
if (mime == MimeTypes.anyImage) {
|
||||||
_icon = AIcons.video;
|
|
||||||
} else if (lowMime == 'image') {
|
|
||||||
_label = 'Image';
|
|
||||||
_icon = AIcons.image;
|
_icon = AIcons.image;
|
||||||
|
} else if (mime == MimeTypes.anyVideo) {
|
||||||
|
_icon = AIcons.video;
|
||||||
}
|
}
|
||||||
_label ??= lowMime.split('/')[0].toUpperCase();
|
|
||||||
} else {
|
} else {
|
||||||
_test = (entry) => entry.mimeType == lowMime;
|
_test = (entry) => entry.mimeType == lowMime;
|
||||||
_label = MimeUtils.displayType(lowMime);
|
_label = MimeUtils.displayType(lowMime);
|
||||||
|
@ -47,13 +47,28 @@ class MimeFilter extends CollectionFilter {
|
||||||
EntryFilter get test => _test;
|
EntryFilter get test => _test;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => _label;
|
String get universalLabel => _label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLabel(BuildContext context) {
|
||||||
|
switch (mime) {
|
||||||
|
case MimeTypes.anyImage:
|
||||||
|
return context.l10n.filterMimeImageLabel;
|
||||||
|
case MimeTypes.anyVideo:
|
||||||
|
return context.l10n.filterMimeVideoLabel;
|
||||||
|
default:
|
||||||
|
return _label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size);
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => '$type-$mime';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -50,7 +50,7 @@ class QueryFilter extends CollectionFilter {
|
||||||
bool get isUnique => false;
|
bool get isUnique => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => '$query';
|
String get universalLabel => query;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size);
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size);
|
||||||
|
@ -59,7 +59,10 @@ class QueryFilter extends CollectionFilter {
|
||||||
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);
|
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => '$type-$query';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class TagFilter extends CollectionFilter {
|
class TagFilter extends CollectionFilter {
|
||||||
static const type = 'tag';
|
static const type = 'tag';
|
||||||
static const emptyLabel = 'untagged';
|
|
||||||
|
|
||||||
final String tag;
|
final String tag;
|
||||||
EntryFilter _test;
|
EntryFilter _test;
|
||||||
|
@ -36,13 +36,19 @@ class TagFilter extends CollectionFilter {
|
||||||
bool get isUnique => false;
|
bool get isUnique => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => tag.isEmpty ? emptyLabel : tag;
|
String get universalLabel => tag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null;
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => '$type-$tag';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -13,26 +14,26 @@ class TypeFilter extends CollectionFilter {
|
||||||
|
|
||||||
final String itemType;
|
final String itemType;
|
||||||
EntryFilter _test;
|
EntryFilter _test;
|
||||||
String _label;
|
|
||||||
IconData _icon;
|
IconData _icon;
|
||||||
|
|
||||||
TypeFilter(this.itemType) {
|
TypeFilter(this.itemType) {
|
||||||
if (itemType == animated) {
|
switch (itemType) {
|
||||||
_test = (entry) => entry.isAnimated;
|
case animated:
|
||||||
_label = 'Animated';
|
_test = (entry) => entry.isAnimated;
|
||||||
_icon = AIcons.animated;
|
_icon = AIcons.animated;
|
||||||
} else if (itemType == panorama) {
|
break;
|
||||||
_test = (entry) => entry.isImage && entry.is360;
|
case panorama:
|
||||||
_label = 'Panorama';
|
_test = (entry) => entry.isImage && entry.is360;
|
||||||
_icon = AIcons.threesixty;
|
_icon = AIcons.threesixty;
|
||||||
} else if (itemType == sphericalVideo) {
|
break;
|
||||||
_test = (entry) => entry.isVideo && entry.is360;
|
case sphericalVideo:
|
||||||
_label = '360° Video';
|
_test = (entry) => entry.isVideo && entry.is360;
|
||||||
_icon = AIcons.threesixty;
|
_icon = AIcons.threesixty;
|
||||||
} else if (itemType == geotiff) {
|
break;
|
||||||
_test = (entry) => entry.isGeotiff;
|
case geotiff:
|
||||||
_label = 'GeoTIFF';
|
_test = (entry) => entry.isGeotiff;
|
||||||
_icon = AIcons.geo;
|
_icon = AIcons.geo;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,13 +52,32 @@ class TypeFilter extends CollectionFilter {
|
||||||
EntryFilter get test => _test;
|
EntryFilter get test => _test;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => _label;
|
String get universalLabel => itemType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getLabel(BuildContext context) {
|
||||||
|
switch (itemType) {
|
||||||
|
case animated:
|
||||||
|
return context.l10n.filterTypeAnimatedLabel;
|
||||||
|
case panorama:
|
||||||
|
return context.l10n.filterTypePanoramaLabel;
|
||||||
|
case sphericalVideo:
|
||||||
|
return context.l10n.filterTypeSphericalVideoLabel;
|
||||||
|
case geotiff:
|
||||||
|
return context.l10n.filterTypeGeotiffLabel;
|
||||||
|
default:
|
||||||
|
return itemType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size);
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeKey => type;
|
String get category => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => '$type-$itemType';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import 'package:aves/services/geocoding_service.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:geocoder/model.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DateMetadata {
|
class DateMetadata {
|
||||||
|
@ -204,38 +204,3 @@ class AddressDetails {
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
|
||||||
class FavouriteRow {
|
|
||||||
final int contentId;
|
|
||||||
final String path;
|
|
||||||
|
|
||||||
const FavouriteRow({
|
|
||||||
this.contentId,
|
|
||||||
this.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory FavouriteRow.fromMap(Map map) {
|
|
||||||
return FavouriteRow(
|
|
||||||
contentId: map['contentId'],
|
|
||||||
path: map['path'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
|
||||||
'contentId': contentId,
|
|
||||||
'path': path,
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is FavouriteRow && other.contentId == contentId && other.path == path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues(contentId, path);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, path=$path}';
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,85 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/metadata.dart';
|
import 'package:aves/model/metadata.dart';
|
||||||
import 'package:aves/model/metadata_db_upgrade.dart';
|
import 'package:aves/model/metadata_db_upgrade.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
final MetadataDb metadataDb = MetadataDb._private();
|
abstract class MetadataDb {
|
||||||
|
Future<void> init();
|
||||||
|
|
||||||
class MetadataDb {
|
Future<int> dbFileSize();
|
||||||
|
|
||||||
|
Future<void> reset();
|
||||||
|
|
||||||
|
Future<void> removeIds(Set<int> contentIds, {@required bool metadataOnly});
|
||||||
|
|
||||||
|
// entries
|
||||||
|
|
||||||
|
Future<void> clearEntries();
|
||||||
|
|
||||||
|
Future<Set<AvesEntry>> loadEntries();
|
||||||
|
|
||||||
|
Future<void> saveEntries(Iterable<AvesEntry> entries);
|
||||||
|
|
||||||
|
Future<void> updateEntryId(int oldId, AvesEntry entry);
|
||||||
|
|
||||||
|
// date taken
|
||||||
|
|
||||||
|
Future<void> clearDates();
|
||||||
|
|
||||||
|
Future<List<DateMetadata>> loadDates();
|
||||||
|
|
||||||
|
// catalog metadata
|
||||||
|
|
||||||
|
Future<void> clearMetadataEntries();
|
||||||
|
|
||||||
|
Future<List<CatalogMetadata>> loadMetadataEntries();
|
||||||
|
|
||||||
|
Future<void> saveMetadata(Iterable<CatalogMetadata> metadataEntries);
|
||||||
|
|
||||||
|
Future<void> updateMetadataId(int oldId, CatalogMetadata metadata);
|
||||||
|
|
||||||
|
// address
|
||||||
|
|
||||||
|
Future<void> clearAddresses();
|
||||||
|
|
||||||
|
Future<List<AddressDetails>> loadAddresses();
|
||||||
|
|
||||||
|
Future<void> saveAddresses(Iterable<AddressDetails> addresses);
|
||||||
|
|
||||||
|
Future<void> updateAddressId(int oldId, AddressDetails address);
|
||||||
|
|
||||||
|
// favourites
|
||||||
|
|
||||||
|
Future<void> clearFavourites();
|
||||||
|
|
||||||
|
Future<Set<FavouriteRow>> loadFavourites();
|
||||||
|
|
||||||
|
Future<void> addFavourites(Iterable<FavouriteRow> rows);
|
||||||
|
|
||||||
|
Future<void> updateFavouriteId(int oldId, FavouriteRow row);
|
||||||
|
|
||||||
|
Future<void> removeFavourites(Iterable<FavouriteRow> rows);
|
||||||
|
|
||||||
|
// covers
|
||||||
|
|
||||||
|
Future<void> clearCovers();
|
||||||
|
|
||||||
|
Future<Set<CoverRow>> loadCovers();
|
||||||
|
|
||||||
|
Future<void> addCovers(Iterable<CoverRow> rows);
|
||||||
|
|
||||||
|
Future<void> updateCoverEntryId(int oldId, CoverRow row);
|
||||||
|
|
||||||
|
Future<void> removeCovers(Iterable<CoverRow> rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqfliteMetadataDb implements MetadataDb {
|
||||||
Future<Database> _database;
|
Future<Database> _database;
|
||||||
|
|
||||||
Future<String> get path async => join(await getDatabasesPath(), 'metadata.db');
|
Future<String> get path async => join(await getDatabasesPath(), 'metadata.db');
|
||||||
|
@ -19,9 +89,9 @@ class MetadataDb {
|
||||||
static const metadataTable = 'metadata';
|
static const metadataTable = 'metadata';
|
||||||
static const addressTable = 'address';
|
static const addressTable = 'address';
|
||||||
static const favouriteTable = 'favourites';
|
static const favouriteTable = 'favourites';
|
||||||
|
static const coverTable = 'covers';
|
||||||
|
|
||||||
MetadataDb._private();
|
@override
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
debugPrint('$runtimeType init');
|
debugPrint('$runtimeType init');
|
||||||
_database = openDatabase(
|
_database = openDatabase(
|
||||||
|
@ -68,17 +138,23 @@ class MetadataDb {
|
||||||
'contentId INTEGER PRIMARY KEY'
|
'contentId INTEGER PRIMARY KEY'
|
||||||
', path TEXT'
|
', path TEXT'
|
||||||
')');
|
')');
|
||||||
|
await db.execute('CREATE TABLE $coverTable('
|
||||||
|
'filter TEXT PRIMARY KEY'
|
||||||
|
', contentId INTEGER'
|
||||||
|
')');
|
||||||
},
|
},
|
||||||
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
||||||
version: 3,
|
version: 4,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<int> dbFileSize() async {
|
Future<int> dbFileSize() async {
|
||||||
final file = File((await path));
|
final file = File((await path));
|
||||||
return await file.exists() ? file.length() : 0;
|
return await file.exists() ? file.length() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> reset() async {
|
Future<void> reset() async {
|
||||||
debugPrint('$runtimeType reset');
|
debugPrint('$runtimeType reset');
|
||||||
await (await _database).close();
|
await (await _database).close();
|
||||||
|
@ -86,7 +162,8 @@ class MetadataDb {
|
||||||
await init();
|
await init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeIds(Set<int> contentIds, {@required bool updateFavourites}) async {
|
@override
|
||||||
|
Future<void> removeIds(Set<int> contentIds, {@required bool metadataOnly}) async {
|
||||||
if (contentIds == null || contentIds.isEmpty) return;
|
if (contentIds == null || contentIds.isEmpty) return;
|
||||||
|
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
@ -100,8 +177,9 @@ class MetadataDb {
|
||||||
batch.delete(dateTakenTable, where: where, whereArgs: whereArgs);
|
batch.delete(dateTakenTable, where: where, whereArgs: whereArgs);
|
||||||
batch.delete(metadataTable, where: where, whereArgs: whereArgs);
|
batch.delete(metadataTable, where: where, whereArgs: whereArgs);
|
||||||
batch.delete(addressTable, where: where, whereArgs: whereArgs);
|
batch.delete(addressTable, where: where, whereArgs: whereArgs);
|
||||||
if (updateFavourites) {
|
if (!metadataOnly) {
|
||||||
batch.delete(favouriteTable, where: where, whereArgs: whereArgs);
|
batch.delete(favouriteTable, where: where, whereArgs: whereArgs);
|
||||||
|
batch.delete(coverTable, where: where, whereArgs: whereArgs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await batch.commit(noResult: true);
|
await batch.commit(noResult: true);
|
||||||
|
@ -110,12 +188,14 @@ class MetadataDb {
|
||||||
|
|
||||||
// entries
|
// entries
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> clearEntries() async {
|
Future<void> clearEntries() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final count = await db.delete(entryTable, where: '1');
|
final count = await db.delete(entryTable, where: '1');
|
||||||
debugPrint('$runtimeType clearEntries deleted $count entries');
|
debugPrint('$runtimeType clearEntries deleted $count entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<Set<AvesEntry>> loadEntries() async {
|
Future<Set<AvesEntry>> loadEntries() async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
|
@ -125,6 +205,7 @@ class MetadataDb {
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> saveEntries(Iterable<AvesEntry> entries) async {
|
Future<void> saveEntries(Iterable<AvesEntry> entries) async {
|
||||||
if (entries == null || entries.isEmpty) return;
|
if (entries == null || entries.isEmpty) return;
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
@ -135,6 +216,7 @@ class MetadataDb {
|
||||||
debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries');
|
debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> updateEntryId(int oldId, AvesEntry entry) async {
|
Future<void> updateEntryId(int oldId, AvesEntry entry) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final batch = db.batch();
|
final batch = db.batch();
|
||||||
|
@ -154,12 +236,14 @@ class MetadataDb {
|
||||||
|
|
||||||
// date taken
|
// date taken
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> clearDates() async {
|
Future<void> clearDates() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final count = await db.delete(dateTakenTable, where: '1');
|
final count = await db.delete(dateTakenTable, where: '1');
|
||||||
debugPrint('$runtimeType clearDates deleted $count entries');
|
debugPrint('$runtimeType clearDates deleted $count entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<List<DateMetadata>> loadDates() async {
|
Future<List<DateMetadata>> loadDates() async {
|
||||||
// final stopwatch = Stopwatch()..start();
|
// final stopwatch = Stopwatch()..start();
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
|
@ -171,12 +255,14 @@ class MetadataDb {
|
||||||
|
|
||||||
// catalog metadata
|
// catalog metadata
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> clearMetadataEntries() async {
|
Future<void> clearMetadataEntries() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final count = await db.delete(metadataTable, where: '1');
|
final count = await db.delete(metadataTable, where: '1');
|
||||||
debugPrint('$runtimeType clearMetadataEntries deleted $count entries');
|
debugPrint('$runtimeType clearMetadataEntries deleted $count entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<List<CatalogMetadata>> loadMetadataEntries() async {
|
Future<List<CatalogMetadata>> loadMetadataEntries() async {
|
||||||
// final stopwatch = Stopwatch()..start();
|
// final stopwatch = Stopwatch()..start();
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
|
@ -186,6 +272,7 @@ class MetadataDb {
|
||||||
return metadataEntries;
|
return metadataEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> saveMetadata(Iterable<CatalogMetadata> metadataEntries) async {
|
Future<void> saveMetadata(Iterable<CatalogMetadata> metadataEntries) async {
|
||||||
if (metadataEntries == null || metadataEntries.isEmpty) return;
|
if (metadataEntries == null || metadataEntries.isEmpty) return;
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
@ -200,6 +287,7 @@ class MetadataDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> updateMetadataId(int oldId, CatalogMetadata metadata) async {
|
Future<void> updateMetadataId(int oldId, CatalogMetadata metadata) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final batch = db.batch();
|
final batch = db.batch();
|
||||||
|
@ -227,12 +315,14 @@ class MetadataDb {
|
||||||
|
|
||||||
// address
|
// address
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> clearAddresses() async {
|
Future<void> clearAddresses() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final count = await db.delete(addressTable, where: '1');
|
final count = await db.delete(addressTable, where: '1');
|
||||||
debugPrint('$runtimeType clearAddresses deleted $count entries');
|
debugPrint('$runtimeType clearAddresses deleted $count entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<List<AddressDetails>> loadAddresses() async {
|
Future<List<AddressDetails>> loadAddresses() async {
|
||||||
// final stopwatch = Stopwatch()..start();
|
// final stopwatch = Stopwatch()..start();
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
|
@ -242,6 +332,7 @@ class MetadataDb {
|
||||||
return addresses;
|
return addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> saveAddresses(Iterable<AddressDetails> addresses) async {
|
Future<void> saveAddresses(Iterable<AddressDetails> addresses) async {
|
||||||
if (addresses == null || addresses.isEmpty) return;
|
if (addresses == null || addresses.isEmpty) return;
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
@ -252,6 +343,7 @@ class MetadataDb {
|
||||||
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
|
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> updateAddressId(int oldId, AddressDetails address) async {
|
Future<void> updateAddressId(int oldId, AddressDetails address) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final batch = db.batch();
|
final batch = db.batch();
|
||||||
|
@ -271,31 +363,31 @@ class MetadataDb {
|
||||||
|
|
||||||
// favourites
|
// favourites
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> clearFavourites() async {
|
Future<void> clearFavourites() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final count = await db.delete(favouriteTable, where: '1');
|
final count = await db.delete(favouriteTable, where: '1');
|
||||||
debugPrint('$runtimeType clearFavourites deleted $count entries');
|
debugPrint('$runtimeType clearFavourites deleted $count entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<FavouriteRow>> loadFavourites() async {
|
@override
|
||||||
// final stopwatch = Stopwatch()..start();
|
Future<Set<FavouriteRow>> loadFavourites() async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final maps = await db.query(favouriteTable);
|
final maps = await db.query(favouriteTable);
|
||||||
final favouriteRows = maps.map((map) => FavouriteRow.fromMap(map)).toList();
|
final rows = maps.map((map) => FavouriteRow.fromMap(map)).toSet();
|
||||||
// debugPrint('$runtimeType loadFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
|
return rows;
|
||||||
return favouriteRows;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addFavourites(Iterable<FavouriteRow> favouriteRows) async {
|
@override
|
||||||
if (favouriteRows == null || favouriteRows.isEmpty) return;
|
Future<void> addFavourites(Iterable<FavouriteRow> rows) async {
|
||||||
// final stopwatch = Stopwatch()..start();
|
if (rows == null || rows.isEmpty) return;
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final batch = db.batch();
|
final batch = db.batch();
|
||||||
favouriteRows.where((row) => row != null).forEach((row) => _batchInsertFavourite(batch, row));
|
rows.where((row) => row != null).forEach((row) => _batchInsertFavourite(batch, row));
|
||||||
await batch.commit(noResult: true);
|
await batch.commit(noResult: true);
|
||||||
// debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> updateFavouriteId(int oldId, FavouriteRow row) async {
|
Future<void> updateFavouriteId(int oldId, FavouriteRow row) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final batch = db.batch();
|
final batch = db.batch();
|
||||||
|
@ -313,9 +405,10 @@ class MetadataDb {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavourites(Iterable<FavouriteRow> favouriteRows) async {
|
@override
|
||||||
if (favouriteRows == null || favouriteRows.isEmpty) return;
|
Future<void> removeFavourites(Iterable<FavouriteRow> rows) async {
|
||||||
final ids = favouriteRows.where((row) => row != null).map((row) => row.contentId);
|
if (rows == null || rows.isEmpty) return;
|
||||||
|
final ids = rows.where((row) => row != null).map((row) => row.contentId);
|
||||||
if (ids.isEmpty) return;
|
if (ids.isEmpty) return;
|
||||||
|
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
|
@ -324,4 +417,61 @@ class MetadataDb {
|
||||||
ids.forEach((id) => batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [id]));
|
ids.forEach((id) => batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [id]));
|
||||||
await batch.commit(noResult: true);
|
await batch.commit(noResult: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// covers
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearCovers() async {
|
||||||
|
final db = await _database;
|
||||||
|
final count = await db.delete(coverTable, where: '1');
|
||||||
|
debugPrint('$runtimeType clearCovers deleted $count entries');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Set<CoverRow>> loadCovers() async {
|
||||||
|
final db = await _database;
|
||||||
|
final maps = await db.query(coverTable);
|
||||||
|
final rows = maps.map((map) => CoverRow.fromMap(map)).toSet();
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addCovers(Iterable<CoverRow> rows) async {
|
||||||
|
if (rows == null || rows.isEmpty) return;
|
||||||
|
final db = await _database;
|
||||||
|
final batch = db.batch();
|
||||||
|
rows.where((row) => row != null).forEach((row) => _batchInsertCover(batch, row));
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateCoverEntryId(int oldId, CoverRow row) async {
|
||||||
|
final db = await _database;
|
||||||
|
final batch = db.batch();
|
||||||
|
batch.delete(coverTable, where: 'contentId = ?', whereArgs: [oldId]);
|
||||||
|
_batchInsertCover(batch, row);
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _batchInsertCover(Batch batch, CoverRow row) {
|
||||||
|
if (row == null) return;
|
||||||
|
batch.insert(
|
||||||
|
coverTable,
|
||||||
|
row.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeCovers(Iterable<CoverRow> rows) async {
|
||||||
|
if (rows == null || rows.isEmpty) return;
|
||||||
|
final filters = rows.where((row) => row != null).map((row) => row.filter);
|
||||||
|
if (filters.isEmpty) return;
|
||||||
|
|
||||||
|
final db = await _database;
|
||||||
|
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead
|
||||||
|
final batch = db.batch();
|
||||||
|
filters.forEach((filter) => batch.delete(coverTable, where: 'filter = ?', whereArgs: [filter.toJson()]));
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
class MetadataDbUpgrader {
|
class MetadataDbUpgrader {
|
||||||
static const entryTable = MetadataDb.entryTable;
|
static const entryTable = SqfliteMetadataDb.entryTable;
|
||||||
static const metadataTable = MetadataDb.metadataTable;
|
static const metadataTable = SqfliteMetadataDb.metadataTable;
|
||||||
|
static const coverTable = SqfliteMetadataDb.coverTable;
|
||||||
|
|
||||||
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
|
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
|
||||||
// on SQLite <3.25.0, bundled on older Android devices
|
// on SQLite <3.25.0, bundled on older Android devices
|
||||||
|
@ -17,6 +18,9 @@ class MetadataDbUpgrader {
|
||||||
case 2:
|
case 2:
|
||||||
await _upgradeFrom2(db);
|
await _upgradeFrom2(db);
|
||||||
break;
|
break;
|
||||||
|
case 3:
|
||||||
|
await _upgradeFrom3(db);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
oldVersion++;
|
oldVersion++;
|
||||||
}
|
}
|
||||||
|
@ -97,4 +101,12 @@ class MetadataDbUpgrader {
|
||||||
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _upgradeFrom3(Database db) async {
|
||||||
|
debugPrint('upgrading DB from v3');
|
||||||
|
await db.execute('CREATE TABLE $coverTable('
|
||||||
|
'filter TEXT PRIMARY KEY'
|
||||||
|
', contentId INTEGER'
|
||||||
|
')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ class SinglePageInfo implements Comparable<SinglePageInfo> {
|
||||||
final index = map['page'] as int;
|
final index = map['page'] as int;
|
||||||
return SinglePageInfo(
|
return SinglePageInfo(
|
||||||
index: index,
|
index: index,
|
||||||
pageId: map['trackId'] as int ?? index,
|
pageId: index,
|
||||||
mimeType: map['mimeType'] as String,
|
mimeType: map['mimeType'] as String,
|
||||||
isDefault: map['isDefault'] as bool ?? false,
|
isDefault: map['isDefault'] as bool ?? false,
|
||||||
width: map['width'] as int ?? 0,
|
width: map['width'] as int ?? 0,
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import 'package:aves/geo/format.dart';
|
import 'package:aves/geo/format.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:latlong/latlong.dart';
|
import 'package:latlong/latlong.dart';
|
||||||
|
|
||||||
enum CoordinateFormat { dms, decimal }
|
import 'enums.dart';
|
||||||
|
|
||||||
extension ExtraCoordinateFormat on CoordinateFormat {
|
extension ExtraCoordinateFormat on CoordinateFormat {
|
||||||
String get name {
|
String getName(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case CoordinateFormat.dms:
|
case CoordinateFormat.dms:
|
||||||
return 'DMS';
|
return context.l10n.coordinateFormatDms;
|
||||||
case CoordinateFormat.decimal:
|
case CoordinateFormat.decimal:
|
||||||
return 'Decimal degrees';
|
return context.l10n.coordinateFormatDecimal;
|
||||||
default:
|
default:
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum EntryBackground { black, white, transparent, checkered }
|
import 'enums.dart';
|
||||||
|
|
||||||
extension ExtraEntryBackground on EntryBackground {
|
extension ExtraEntryBackground on EntryBackground {
|
||||||
bool get isColor {
|
bool get isColor {
|
||||||
|
|
10
lib/model/settings/enums.dart
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
enum CoordinateFormat { dms, decimal }
|
||||||
|
|
||||||
|
enum EntryBackground { black, white, transparent, checkered }
|
||||||
|
|
||||||
|
enum HomePageSetting { collection, albums }
|
||||||
|
|
||||||
|
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
|
||||||
|
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor }
|
||||||
|
|
||||||
|
enum KeepScreenOn { never, viewerOnly, always }
|
|
@ -1,15 +1,17 @@
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum HomePageSetting { collection, albums }
|
import 'enums.dart';
|
||||||
|
|
||||||
extension ExtraHomePageSetting on HomePageSetting {
|
extension ExtraHomePageSetting on HomePageSetting {
|
||||||
String get name {
|
String getName(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case HomePageSetting.collection:
|
case HomePageSetting.collection:
|
||||||
return 'Collection';
|
return context.l10n.collectionPageTitle;
|
||||||
case HomePageSetting.albums:
|
case HomePageSetting.albums:
|
||||||
return 'Albums';
|
return context.l10n.albumPageTitle;
|
||||||
default:
|
default:
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor }
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'enums.dart';
|
||||||
|
|
||||||
extension ExtraEntryMapStyle on EntryMapStyle {
|
extension ExtraEntryMapStyle on EntryMapStyle {
|
||||||
String get name {
|
String getName(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case EntryMapStyle.googleNormal:
|
case EntryMapStyle.googleNormal:
|
||||||
return 'Google Maps';
|
return context.l10n.mapStyleGoogleNormal;
|
||||||
case EntryMapStyle.googleHybrid:
|
case EntryMapStyle.googleHybrid:
|
||||||
return 'Google Maps (Hybrid)';
|
return context.l10n.mapStyleGoogleHybrid;
|
||||||
case EntryMapStyle.googleTerrain:
|
case EntryMapStyle.googleTerrain:
|
||||||
return 'Google Maps (Terrain)';
|
return context.l10n.mapStyleGoogleTerrain;
|
||||||
case EntryMapStyle.osmHot:
|
case EntryMapStyle.osmHot:
|
||||||
return 'Humanitarian OSM';
|
return context.l10n.mapStyleOsmHot;
|
||||||
case EntryMapStyle.stamenToner:
|
case EntryMapStyle.stamenToner:
|
||||||
return 'Stamen Toner';
|
return context.l10n.mapStyleStamenToner;
|
||||||
case EntryMapStyle.stamenWatercolor:
|
case EntryMapStyle.stamenWatercolor:
|
||||||
return 'Stamen Watercolor';
|
return context.l10n.mapStyleStamenWatercolor;
|
||||||
default:
|
default:
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import 'package:aves/services/window_service.dart';
|
import 'package:aves/services/window_service.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum KeepScreenOn { never, viewerOnly, always }
|
import 'enums.dart';
|
||||||
|
|
||||||
extension ExtraKeepScreenOn on KeepScreenOn {
|
extension ExtraKeepScreenOn on KeepScreenOn {
|
||||||
String get name {
|
String getName(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case KeepScreenOn.never:
|
case KeepScreenOn.never:
|
||||||
return 'Never';
|
return context.l10n.keepScreenOnNever;
|
||||||
case KeepScreenOn.viewerOnly:
|
case KeepScreenOn.viewerOnly:
|
||||||
return 'Viewer page only';
|
return context.l10n.keepScreenOnViewerOnly;
|
||||||
case KeepScreenOn.always:
|
case KeepScreenOn.always:
|
||||||
return 'Always';
|
return context.l10n.keepScreenOnAlways;
|
||||||
default:
|
default:
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/coordinate_format.dart';
|
|
||||||
import 'package:aves/model/settings/entry_background.dart';
|
|
||||||
import 'package:aves/model/settings/home_page.dart';
|
|
||||||
import 'package:aves/model/settings/map_style.dart';
|
|
||||||
import 'package:aves/model/settings/screen_on.dart';
|
import 'package:aves/model/settings/screen_on.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../source/enums.dart';
|
import '../source/enums.dart';
|
||||||
|
import 'enums.dart';
|
||||||
|
|
||||||
final Settings settings = Settings._private();
|
final Settings settings = Settings._private();
|
||||||
|
|
||||||
|
@ -24,6 +20,7 @@ class Settings extends ChangeNotifier {
|
||||||
// app
|
// app
|
||||||
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
||||||
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
|
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
|
||||||
|
static const localeKey = 'locale';
|
||||||
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
||||||
static const keepScreenOnKey = 'keep_screen_on';
|
static const keepScreenOnKey = 'keep_screen_on';
|
||||||
static const homePageKey = 'home_page';
|
static const homePageKey = 'home_page';
|
||||||
|
@ -99,6 +96,34 @@ class Settings extends ChangeNotifier {
|
||||||
unawaited(initFirebase());
|
unawaited(initFirebase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const localeSeparator = '-';
|
||||||
|
|
||||||
|
Locale get locale {
|
||||||
|
// exceptionally allow getting locale before settings are initialized
|
||||||
|
final tag = _prefs?.getString(localeKey);
|
||||||
|
if (tag != null) {
|
||||||
|
final codes = tag.split(localeSeparator);
|
||||||
|
return Locale.fromSubtags(
|
||||||
|
languageCode: codes[0],
|
||||||
|
scriptCode: codes[1] == '' ? null : codes[1],
|
||||||
|
countryCode: codes[2] == '' ? null : codes[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set locale(Locale newValue) {
|
||||||
|
String tag;
|
||||||
|
if (newValue != null) {
|
||||||
|
tag = [
|
||||||
|
newValue.languageCode ?? '',
|
||||||
|
newValue.scriptCode ?? '',
|
||||||
|
newValue.countryCode ?? '',
|
||||||
|
].join(localeSeparator);
|
||||||
|
}
|
||||||
|
setAndNotify(localeKey, tag);
|
||||||
|
}
|
||||||
|
|
||||||
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
|
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
|
||||||
|
|
||||||
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
|
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
|
||||||
|
@ -120,7 +145,7 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0;
|
double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0;
|
||||||
|
|
||||||
// do not notify, as tile extents are only used internally by `TileExtentManager`
|
// do not notify, as tile extents are only used internally by `TileExtentController`
|
||||||
// and should not trigger rebuilding by change notification
|
// and should not trigger rebuilding by change notification
|
||||||
void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue, notify: false);
|
void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue, notify: false);
|
||||||
|
|
||||||
|
@ -182,7 +207,7 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue);
|
set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue);
|
||||||
|
|
||||||
bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, true);
|
bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, false);
|
||||||
|
|
||||||
set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue);
|
set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
mixin AlbumMixin on SourceBase {
|
mixin AlbumMixin on SourceBase {
|
||||||
|
@ -12,8 +13,8 @@ mixin AlbumMixin on SourceBase {
|
||||||
List<String> get rawAlbums => List.unmodifiable(_directories);
|
List<String> get rawAlbums => List.unmodifiable(_directories);
|
||||||
|
|
||||||
int compareAlbumsByName(String a, String b) {
|
int compareAlbumsByName(String a, String b) {
|
||||||
final ua = getUniqueAlbumName(a);
|
final ua = getUniqueAlbumName(null, a);
|
||||||
final ub = getUniqueAlbumName(b);
|
final ub = getUniqueAlbumName(null, b);
|
||||||
final c = compareAsciiUpperCase(ua, ub);
|
final c = compareAsciiUpperCase(ua, ub);
|
||||||
if (c != 0) return c;
|
if (c != 0) return c;
|
||||||
final va = androidFileUtils.getStorageVolume(a)?.path ?? '';
|
final va = androidFileUtils.getStorageVolume(a)?.path ?? '';
|
||||||
|
@ -23,7 +24,7 @@ mixin AlbumMixin on SourceBase {
|
||||||
|
|
||||||
void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent());
|
void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent());
|
||||||
|
|
||||||
String getUniqueAlbumName(String dirPath) {
|
String getUniqueAlbumName(BuildContext context, String dirPath) {
|
||||||
String unique(String dirPath, [bool Function(String) test]) {
|
String unique(String dirPath, [bool Function(String) test]) {
|
||||||
final otherAlbums = _directories.where(test ?? (_) => true).where((item) => item != dirPath);
|
final otherAlbums = _directories.where(test ?? (_) => true).where((item) => item != dirPath);
|
||||||
final parts = dirPath.split(separator);
|
final parts = dirPath.split(separator);
|
||||||
|
@ -51,7 +52,7 @@ mixin AlbumMixin on SourceBase {
|
||||||
if (volume.isPrimary) {
|
if (volume.isPrimary) {
|
||||||
return uniqueNameInVolume;
|
return uniqueNameInVolume;
|
||||||
} else {
|
} else {
|
||||||
return '$uniqueNameInVolume (${volume.description})';
|
return '$uniqueNameInVolume (${volume.getDescription(context)})';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +100,7 @@ mixin AlbumMixin on SourceBase {
|
||||||
invalidateAlbumFilterSummary(directories: emptyAlbums);
|
invalidateAlbumFilterSummary(directories: emptyAlbums);
|
||||||
|
|
||||||
final pinnedFilters = settings.pinnedFilters;
|
final pinnedFilters = settings.pinnedFilters;
|
||||||
emptyAlbums.forEach((album) => pinnedFilters.remove(AlbumFilter(album, getUniqueAlbumName(album))));
|
emptyAlbums.forEach((album) => pinnedFilters.removeWhere((filter) => filter is AlbumFilter && filter.album == album));
|
||||||
settings.pinnedFilters = pinnedFilters;
|
settings.pinnedFilters = pinnedFilters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin {
|
||||||
void addFilter(CollectionFilter filter) {
|
void addFilter(CollectionFilter filter) {
|
||||||
if (filter == null || filters.contains(filter)) return;
|
if (filter == null || filters.contains(filter)) return;
|
||||||
if (filter.isUnique) {
|
if (filter.isUnique) {
|
||||||
filters.removeWhere((old) => old.typeKey == filter.typeKey);
|
filters.removeWhere((old) => old.category == filter.category);
|
||||||
}
|
}
|
||||||
filters.add(filter);
|
filters.add(filter);
|
||||||
onFilterChanged();
|
onFilterChanged();
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/metadata.dart';
|
import 'package:aves/model/metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/album.dart';
|
import 'package:aves/model/source/album.dart';
|
||||||
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/model/source/location.dart';
|
import 'package:aves/model/source/location.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/services/image_op_events.dart';
|
import 'package:aves/services/image_op_events.dart';
|
||||||
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
@ -98,10 +100,11 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
eventBus.fire(EntryAddedEvent(entries));
|
eventBus.fire(EntryAddedEvent(entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeEntries(Set<String> uris) {
|
Future<void> removeEntries(Set<String> uris) async {
|
||||||
if (uris.isEmpty) return;
|
if (uris.isEmpty) return;
|
||||||
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
|
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
|
||||||
entries.forEach((entry) => entry.removeFromFavourites());
|
await favourites.remove(entries);
|
||||||
|
await covers.removeEntries(entries);
|
||||||
_rawEntries.removeAll(entries);
|
_rawEntries.removeAll(entries);
|
||||||
_invalidate(entries);
|
_invalidate(entries);
|
||||||
|
|
||||||
|
@ -120,30 +123,61 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
updateTags();
|
updateTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _moveEntry(AvesEntry entry, Map newFields, bool isFavourite) async {
|
Future<void> _moveEntry(AvesEntry entry, Map newFields) async {
|
||||||
final oldContentId = entry.contentId;
|
final oldContentId = entry.contentId;
|
||||||
final newContentId = newFields['contentId'] as int;
|
final newContentId = newFields['contentId'] as int;
|
||||||
final newDateModifiedSecs = newFields['dateModifiedSecs'] as int;
|
|
||||||
|
entry.contentId = newContentId;
|
||||||
// `dateModifiedSecs` changes when moving entries to another directory,
|
// `dateModifiedSecs` changes when moving entries to another directory,
|
||||||
// but it does not change when renaming the containing directory
|
// but it does not change when renaming the containing directory
|
||||||
if (newDateModifiedSecs != null) entry.dateModifiedSecs = newDateModifiedSecs;
|
if (newFields.containsKey('dateModifiedSecs')) entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int;
|
||||||
entry.path = newFields['path'] as String;
|
if (newFields.containsKey('path')) entry.path = newFields['path'] as String;
|
||||||
entry.uri = newFields['uri'] as String;
|
if (newFields.containsKey('uri')) entry.uri = newFields['uri'] as String;
|
||||||
entry.contentId = newContentId;
|
if (newFields.containsKey('title') != null) entry.sourceTitle = newFields['title'] as String;
|
||||||
|
|
||||||
entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId);
|
entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId);
|
||||||
entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId);
|
entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId);
|
||||||
|
|
||||||
await metadataDb.updateEntryId(oldContentId, entry);
|
await metadataDb.updateEntryId(oldContentId, entry);
|
||||||
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
await metadataDb.updateMetadataId(oldContentId, entry.catalogMetadata);
|
||||||
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
await metadataDb.updateAddressId(oldContentId, entry.addressDetails);
|
||||||
if (isFavourite) {
|
await favourites.moveEntry(oldContentId, entry);
|
||||||
await favourites.move(oldContentId, entry);
|
await covers.moveEntry(oldContentId, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> renameEntry(AvesEntry entry, String newName) async {
|
||||||
|
if (newName == entry.filenameWithoutExtension) return true;
|
||||||
|
final newFields = await imageFileService.rename(entry, '$newName${entry.extension}');
|
||||||
|
if (newFields.isEmpty) return false;
|
||||||
|
|
||||||
|
await _moveEntry(entry, newFields);
|
||||||
|
entry.metadataChangeNotifier.notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> renameAlbum(String sourceAlbum, String destinationAlbum, Set<AvesEntry> todoEntries, Set<MoveOpEvent> movedOps) async {
|
||||||
|
final oldFilter = AlbumFilter(sourceAlbum, null);
|
||||||
|
final pinned = settings.pinnedFilters.contains(oldFilter);
|
||||||
|
final oldCoverContentId = covers.coverContentId(oldFilter);
|
||||||
|
final coverEntry = oldCoverContentId != null ? todoEntries.firstWhere((entry) => entry.contentId == oldCoverContentId, orElse: () => null) : null;
|
||||||
|
await updateAfterMove(
|
||||||
|
todoEntries: todoEntries,
|
||||||
|
copy: false,
|
||||||
|
destinationAlbum: destinationAlbum,
|
||||||
|
movedOps: movedOps,
|
||||||
|
);
|
||||||
|
// restore pin and cover, as the obsolete album got removed and its associated state cleaned
|
||||||
|
final newFilter = AlbumFilter(destinationAlbum, null);
|
||||||
|
if (pinned) {
|
||||||
|
settings.pinnedFilters = settings.pinnedFilters..add(newFilter);
|
||||||
|
}
|
||||||
|
if (coverEntry != null) {
|
||||||
|
await covers.set(newFilter, coverEntry.contentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateAfterMove({
|
Future<void> updateAfterMove({
|
||||||
@required Set<AvesEntry> todoEntries,
|
@required Set<AvesEntry> todoEntries,
|
||||||
@required Set<AvesEntry> favouriteEntries,
|
|
||||||
@required bool copy,
|
@required bool copy,
|
||||||
@required String destinationAlbum,
|
@required String destinationAlbum,
|
||||||
@required Set<MoveOpEvent> movedOps,
|
@required Set<MoveOpEvent> movedOps,
|
||||||
|
@ -177,10 +211,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
fromAlbums.add(entry.directory);
|
fromAlbums.add(entry.directory);
|
||||||
movedEntries.add(entry);
|
movedEntries.add(entry);
|
||||||
// do not rely on current favourite repo state to assess whether the moved entry is a favourite
|
await _moveEntry(entry, newFields);
|
||||||
// as source monitoring may already have removed the entry from the favourite repo
|
|
||||||
final isFavourite = favouriteEntries.contains(entry);
|
|
||||||
await _moveEntry(entry, newFields, isFavourite);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -231,6 +262,15 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvesEntry coverEntry(CollectionFilter filter) {
|
||||||
|
final contentId = covers.coverContentId(filter);
|
||||||
|
if (contentId != null) {
|
||||||
|
final entry = visibleEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null);
|
||||||
|
if (entry != null) return entry;
|
||||||
|
}
|
||||||
|
return recentEntry(filter);
|
||||||
|
}
|
||||||
|
|
||||||
void changeFilterVisibility(CollectionFilter filter, bool visible) {
|
void changeFilterVisibility(CollectionFilter filter, bool visible) {
|
||||||
final hiddenFilters = settings.hiddenFilters;
|
final hiddenFilters = settings.hiddenFilters;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
@ -256,8 +296,6 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SourceState { loading, cataloguing, locating, ready }
|
|
||||||
|
|
||||||
class EntryAddedEvent {
|
class EntryAddedEvent {
|
||||||
final Set<AvesEntry> entries;
|
final Set<AvesEntry> entries;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
enum Activity { browse, select }
|
enum Activity { browse, select }
|
||||||
|
|
||||||
|
enum SourceState { loading, cataloguing, locating, ready }
|
||||||
|
|
||||||
enum ChipSortFactor { date, name, count }
|
enum ChipSortFactor { date, name, count }
|
||||||
|
|
||||||
enum AlbumChipGroupFactor { none, importance, volume }
|
enum AlbumChipGroupFactor { none, importance, volume }
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/geo/countries.dart';
|
import 'package:aves/geo/countries.dart';
|
||||||
import 'package:aves/model/availability.dart';
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/metadata.dart';
|
import 'package:aves/model/metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/enums.dart';
|
||||||
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
@ -81,14 +81,14 @@ mixin LocationMixin on SourceBase {
|
||||||
// - 652 calls (22%) when approximating to 2 decimal places (~1km - town or village)
|
// - 652 calls (22%) when approximating to 2 decimal places (~1km - town or village)
|
||||||
// cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision
|
// cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision
|
||||||
final latLngFactor = pow(10, 2);
|
final latLngFactor = pow(10, 2);
|
||||||
Tuple2 approximateLatLng(AvesEntry entry) {
|
Tuple2<int, int> approximateLatLng(AvesEntry entry) {
|
||||||
final lat = entry.catalogMetadata?.latitude;
|
final lat = entry.catalogMetadata?.latitude;
|
||||||
final lng = entry.catalogMetadata?.longitude;
|
final lng = entry.catalogMetadata?.longitude;
|
||||||
if (lat == null || lng == null) return null;
|
if (lat == null || lng == null) return null;
|
||||||
return Tuple2((lat * latLngFactor).round(), (lng * latLngFactor).round());
|
return Tuple2<int, int>((lat * latLngFactor).round(), (lng * latLngFactor).round());
|
||||||
}
|
}
|
||||||
|
|
||||||
final knownLocations = <Tuple2, AddressDetails>{};
|
final knownLocations = <Tuple2<int, int>, AddressDetails>{};
|
||||||
byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails));
|
byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails));
|
||||||
|
|
||||||
stateNotifier.value = SourceState.locating;
|
stateNotifier.value = SourceState.locating;
|
||||||
|
@ -138,7 +138,7 @@ mixin LocationMixin on SourceBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
// the same country code could be found with different country names
|
// the same country code could be found with different country names
|
||||||
// e.g. if the locale changed between geolocating calls
|
// e.g. if the locale changed between geocoding calls
|
||||||
// so we merge countries by code, keeping only one name for each code
|
// so we merge countries by code, keeping only one name for each code
|
||||||
final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty));
|
final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty));
|
||||||
final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase);
|
final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase);
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/services/media_store_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:aves/services/time_service.dart';
|
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
@ -26,7 +25,8 @@ class MediaStoreSource extends CollectionSource {
|
||||||
stateNotifier.value = SourceState.loading;
|
stateNotifier.value = SourceState.loading;
|
||||||
await metadataDb.init();
|
await metadataDb.init();
|
||||||
await favourites.init();
|
await favourites.init();
|
||||||
final currentTimeZone = await TimeService.getDefaultTimeZone();
|
await covers.init();
|
||||||
|
final currentTimeZone = await timeService.getDefaultTimeZone();
|
||||||
final catalogTimeZone = settings.catalogTimeZone;
|
final catalogTimeZone = settings.catalogTimeZone;
|
||||||
if (currentTimeZone != catalogTimeZone) {
|
if (currentTimeZone != catalogTimeZone) {
|
||||||
// clear catalog metadata to get correct date/times when moving to a different time zone
|
// clear catalog metadata to get correct date/times when moving to a different time zone
|
||||||
|
@ -50,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
|
|
||||||
final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries
|
final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries
|
||||||
final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs)));
|
final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs)));
|
||||||
final obsoleteContentIds = (await MediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet();
|
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet();
|
||||||
oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId));
|
oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId));
|
||||||
|
|
||||||
// show known entries
|
// show known entries
|
||||||
|
@ -60,11 +60,11 @@ class MediaStoreSource extends CollectionSource {
|
||||||
debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}');
|
debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}');
|
||||||
|
|
||||||
// clean up obsolete entries
|
// clean up obsolete entries
|
||||||
metadataDb.removeIds(obsoleteContentIds, updateFavourites: true);
|
await metadataDb.removeIds(obsoleteContentIds, metadataOnly: false);
|
||||||
|
|
||||||
// verify paths because some apps move files without updating their `last modified date`
|
// verify paths because some apps move files without updating their `last modified date`
|
||||||
final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId, entry.path)));
|
final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId, entry.path)));
|
||||||
final movedContentIds = (await MediaStoreService.checkObsoletePaths(knownPathById)).toSet();
|
final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathById)).toSet();
|
||||||
movedContentIds.forEach((contentId) {
|
movedContentIds.forEach((contentId) {
|
||||||
// make obsolete by resetting its modified date
|
// make obsolete by resetting its modified date
|
||||||
knownDateById[contentId] = 0;
|
knownDateById[contentId] = 0;
|
||||||
|
@ -81,7 +81,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
pendingNewEntries.clear();
|
pendingNewEntries.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaStoreService.getEntries(knownDateById).listen(
|
mediaStoreService.getEntries(knownDateById).listen(
|
||||||
(entry) {
|
(entry) {
|
||||||
pendingNewEntries.add(entry);
|
pendingNewEntries.add(entry);
|
||||||
if (pendingNewEntries.length >= refreshCount) {
|
if (pendingNewEntries.length >= refreshCount) {
|
||||||
|
@ -114,6 +114,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reportCollectionDimensions() {
|
void _reportCollectionDimensions() {
|
||||||
|
if (!settings.isCrashlyticsEnabled) return;
|
||||||
final analytics = FirebaseAnalytics();
|
final analytics = FirebaseAnalytics();
|
||||||
analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(allEntries.length, 3)).toString());
|
analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(allEntries.length, 3)).toString());
|
||||||
analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString());
|
analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString());
|
||||||
|
@ -141,9 +142,9 @@ class MediaStoreSource extends CollectionSource {
|
||||||
}).where((kv) => kv != null));
|
}).where((kv) => kv != null));
|
||||||
|
|
||||||
// clean up obsolete entries
|
// clean up obsolete entries
|
||||||
final obsoleteContentIds = (await MediaStoreService.checkObsoleteContentIds(uriByContentId.keys.toList())).toSet();
|
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(uriByContentId.keys.toList())).toSet();
|
||||||
final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).toSet();
|
final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).toSet();
|
||||||
removeEntries(obsoleteUris);
|
await removeEntries(obsoleteUris);
|
||||||
obsoleteContentIds.forEach(uriByContentId.remove);
|
obsoleteContentIds.forEach(uriByContentId.remove);
|
||||||
|
|
||||||
// fetch new entries
|
// fetch new entries
|
||||||
|
@ -153,7 +154,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
for (final kv in uriByContentId.entries) {
|
for (final kv in uriByContentId.entries) {
|
||||||
final contentId = kv.key;
|
final contentId = kv.key;
|
||||||
final uri = kv.value;
|
final uri = kv.value;
|
||||||
final sourceEntry = await ImageFileService.getEntry(uri, null);
|
final sourceEntry = await imageFileService.getEntry(uri, null);
|
||||||
if (sourceEntry != null) {
|
if (sourceEntry != null) {
|
||||||
final existingEntry = allEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null);
|
final existingEntry = allEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null);
|
||||||
// compare paths because some apps move files without updating their `last modified date`
|
// compare paths because some apps move files without updating their `last modified date`
|
||||||
|
@ -188,7 +189,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
@override
|
@override
|
||||||
Future<void> refreshMetadata(Set<AvesEntry> entries) {
|
Future<void> refreshMetadata(Set<AvesEntry> entries) {
|
||||||
final contentIds = entries.map((entry) => entry.contentId).toSet();
|
final contentIds = entries.map((entry) => entry.contentId).toSet();
|
||||||
metadataDb.removeIds(contentIds, updateFavourites: false);
|
metadataDb.removeIds(contentIds, metadataOnly: true);
|
||||||
return refresh();
|
return refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/metadata.dart';
|
import 'package:aves/model/metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/enums.dart';
|
||||||
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,6 @@ class AndroidAppService {
|
||||||
static Future<bool> edit(String uri, String mimeType) async {
|
static Future<bool> edit(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('edit', <String, dynamic>{
|
return await platform.invokeMethod('edit', <String, dynamic>{
|
||||||
'title': 'Edit with:',
|
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
});
|
});
|
||||||
|
@ -54,7 +53,6 @@ class AndroidAppService {
|
||||||
static Future<bool> open(String uri, String mimeType) async {
|
static Future<bool> open(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('open', <String, dynamic>{
|
return await platform.invokeMethod('open', <String, dynamic>{
|
||||||
'title': 'Open with:',
|
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
});
|
});
|
||||||
|
@ -78,7 +76,6 @@ class AndroidAppService {
|
||||||
static Future<bool> setAs(String uri, String mimeType) async {
|
static Future<bool> setAs(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('setAs', <String, dynamic>{
|
return await platform.invokeMethod('setAs', <String, dynamic>{
|
||||||
'title': 'Set as:',
|
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
});
|
});
|
||||||
|
@ -94,7 +91,6 @@ class AndroidAppService {
|
||||||
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('share', <String, dynamic>{
|
return await platform.invokeMethod('share', <String, dynamic>{
|
||||||
'title': 'Share via:',
|
|
||||||
'urisByMimeType': urisByMimeType,
|
'urisByMimeType': urisByMimeType,
|
||||||
});
|
});
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -106,7 +102,6 @@ class AndroidAppService {
|
||||||
static Future<bool> shareSingle(String uri, String mimeType) async {
|
static Future<bool> shareSingle(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('share', <String, dynamic>{
|
return await platform.invokeMethod('share', <String, dynamic>{
|
||||||
'title': 'Share via:',
|
|
||||||
'urisByMimeType': {
|
'urisByMimeType': {
|
||||||
mimeType: [uri]
|
mimeType: [uri]
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class AppShortcutService {
|
||||||
Uint8List iconBytes;
|
Uint8List iconBytes;
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
final size = entry.isVideo ? 0.0 : 256.0;
|
final size = entry.isVideo ? 0.0 : 256.0;
|
||||||
iconBytes = await ImageFileService.getThumbnail(
|
iconBytes = await imageFileService.getThumbnail(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
pageId: entry.pageId,
|
pageId: entry.pageId,
|
||||||
|
|
61
lib/services/geocoding_service.dart
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:latlong/latlong.dart';
|
||||||
|
|
||||||
|
class GeocodingService {
|
||||||
|
static const platform = MethodChannel('deckers.thibault/aves/geocoding');
|
||||||
|
|
||||||
|
// geocoding requires Google Play Services
|
||||||
|
static Future<List<Address>> getAddress(LatLng coordinates, String locale) async {
|
||||||
|
try {
|
||||||
|
final result = await platform.invokeMethod('getAddress', <String, dynamic>{
|
||||||
|
'latitude': coordinates.latitude,
|
||||||
|
'longitude': coordinates.longitude,
|
||||||
|
'locale': locale,
|
||||||
|
// we only really need one address, but sometimes the native geocoder
|
||||||
|
// returns nothing with `maxResults` of 1, but succeeds with `maxResults` of 2+
|
||||||
|
'maxResults': 2,
|
||||||
|
});
|
||||||
|
return (result as List).cast<Map>().map((map) => Address.fromMap(map)).toList();
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getAddress failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class Address {
|
||||||
|
final String addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare;
|
||||||
|
|
||||||
|
const Address({
|
||||||
|
this.addressLine,
|
||||||
|
this.adminArea,
|
||||||
|
this.countryCode,
|
||||||
|
this.countryName,
|
||||||
|
this.featureName,
|
||||||
|
this.locality,
|
||||||
|
this.postalCode,
|
||||||
|
this.subAdminArea,
|
||||||
|
this.subLocality,
|
||||||
|
this.subThoroughfare,
|
||||||
|
this.thoroughfare,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Address.fromMap(Map map) => Address(
|
||||||
|
addressLine: map['addressLine'],
|
||||||
|
adminArea: map['adminArea'],
|
||||||
|
countryCode: map['countryCode'],
|
||||||
|
countryName: map['countryName'],
|
||||||
|
featureName: map['featureName'],
|
||||||
|
locality: map['locality'],
|
||||||
|
postalCode: map['postalCode'],
|
||||||
|
subAdminArea: map['subAdminArea'],
|
||||||
|
subLocality: map['subLocality'],
|
||||||
|
subThoroughfare: map['subThoroughfare'],
|
||||||
|
thoroughfare: map['thoroughfare'],
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,7 +11,82 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:streams_channel/streams_channel.dart';
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
|
|
||||||
class ImageFileService {
|
abstract class ImageFileService {
|
||||||
|
Future<AvesEntry> getEntry(String uri, String mimeType);
|
||||||
|
|
||||||
|
Future<Uint8List> getSvg(
|
||||||
|
String uri,
|
||||||
|
String mimeType, {
|
||||||
|
int expectedContentLength,
|
||||||
|
BytesReceivedCallback onBytesReceived,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Uint8List> getImage(
|
||||||
|
String uri,
|
||||||
|
String mimeType,
|
||||||
|
int rotationDegrees,
|
||||||
|
bool isFlipped, {
|
||||||
|
int pageId,
|
||||||
|
int expectedContentLength,
|
||||||
|
BytesReceivedCallback onBytesReceived,
|
||||||
|
});
|
||||||
|
|
||||||
|
// `rect`: region to decode, with coordinates in reference to `imageSize`
|
||||||
|
Future<Uint8List> getRegion(
|
||||||
|
String uri,
|
||||||
|
String mimeType,
|
||||||
|
int rotationDegrees,
|
||||||
|
bool isFlipped,
|
||||||
|
int sampleSize,
|
||||||
|
Rectangle<int> regionRect,
|
||||||
|
Size imageSize, {
|
||||||
|
int pageId,
|
||||||
|
Object taskKey,
|
||||||
|
int priority,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Uint8List> getThumbnail({
|
||||||
|
@required String uri,
|
||||||
|
@required String mimeType,
|
||||||
|
@required int rotationDegrees,
|
||||||
|
@required int pageId,
|
||||||
|
@required bool isFlipped,
|
||||||
|
@required int dateModifiedSecs,
|
||||||
|
@required double extent,
|
||||||
|
Object taskKey,
|
||||||
|
int priority,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> clearSizedThumbnailDiskCache();
|
||||||
|
|
||||||
|
bool cancelRegion(Object taskKey);
|
||||||
|
|
||||||
|
bool cancelThumbnail(Object taskKey);
|
||||||
|
|
||||||
|
Future<T> resumeLoading<T>(Object taskKey);
|
||||||
|
|
||||||
|
Stream<ImageOpEvent> delete(Iterable<AvesEntry> entries);
|
||||||
|
|
||||||
|
Stream<MoveOpEvent> move(
|
||||||
|
Iterable<AvesEntry> entries, {
|
||||||
|
@required bool copy,
|
||||||
|
@required String destinationAlbum,
|
||||||
|
});
|
||||||
|
|
||||||
|
Stream<ExportOpEvent> export(
|
||||||
|
Iterable<AvesEntry> entries, {
|
||||||
|
String mimeType = MimeTypes.jpeg,
|
||||||
|
@required String destinationAlbum,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Map> rename(AvesEntry entry, String newName);
|
||||||
|
|
||||||
|
Future<Map> rotate(AvesEntry entry, {@required bool clockwise});
|
||||||
|
|
||||||
|
Future<Map> flip(AvesEntry entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformImageFileService implements ImageFileService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/image');
|
static const platform = MethodChannel('deckers.thibault/aves/image');
|
||||||
static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
|
static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
|
||||||
static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
|
static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
|
||||||
|
@ -31,7 +106,8 @@ class ImageFileService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<AvesEntry> getEntry(String uri, String mimeType) async {
|
@override
|
||||||
|
Future<AvesEntry> getEntry(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getEntry', <String, dynamic>{
|
final result = await platform.invokeMethod('getEntry', <String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
|
@ -44,7 +120,8 @@ class ImageFileService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getSvg(
|
@override
|
||||||
|
Future<Uint8List> getSvg(
|
||||||
String uri,
|
String uri,
|
||||||
String mimeType, {
|
String mimeType, {
|
||||||
int expectedContentLength,
|
int expectedContentLength,
|
||||||
|
@ -59,7 +136,8 @@ class ImageFileService {
|
||||||
onBytesReceived: onBytesReceived,
|
onBytesReceived: onBytesReceived,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<Uint8List> getImage(
|
@override
|
||||||
|
Future<Uint8List> getImage(
|
||||||
String uri,
|
String uri,
|
||||||
String mimeType,
|
String mimeType,
|
||||||
int rotationDegrees,
|
int rotationDegrees,
|
||||||
|
@ -106,8 +184,8 @@ class ImageFileService {
|
||||||
return Future.sync(() => null);
|
return Future.sync(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// `rect`: region to decode, with coordinates in reference to `imageSize`
|
@override
|
||||||
static Future<Uint8List> getRegion(
|
Future<Uint8List> getRegion(
|
||||||
String uri,
|
String uri,
|
||||||
String mimeType,
|
String mimeType,
|
||||||
int rotationDegrees,
|
int rotationDegrees,
|
||||||
|
@ -145,7 +223,8 @@ class ImageFileService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getThumbnail({
|
@override
|
||||||
|
Future<Uint8List> getThumbnail({
|
||||||
@required String uri,
|
@required String uri,
|
||||||
@required String mimeType,
|
@required String mimeType,
|
||||||
@required int rotationDegrees,
|
@required int rotationDegrees,
|
||||||
|
@ -184,7 +263,8 @@ class ImageFileService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> clearSizedThumbnailDiskCache() async {
|
@override
|
||||||
|
Future<void> clearSizedThumbnailDiskCache() async {
|
||||||
try {
|
try {
|
||||||
return platform.invokeMethod('clearSizedThumbnailDiskCache');
|
return platform.invokeMethod('clearSizedThumbnailDiskCache');
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
@ -192,13 +272,17 @@ class ImageFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]);
|
@override
|
||||||
|
bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]);
|
||||||
|
|
||||||
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]);
|
@override
|
||||||
|
bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]);
|
||||||
|
|
||||||
static Future<T> resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
@override
|
||||||
|
Future<T> resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||||
|
|
||||||
static Stream<ImageOpEvent> delete(Iterable<AvesEntry> entries) {
|
@override
|
||||||
|
Stream<ImageOpEvent> delete(Iterable<AvesEntry> entries) {
|
||||||
try {
|
try {
|
||||||
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
|
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
'op': 'delete',
|
'op': 'delete',
|
||||||
|
@ -210,7 +294,8 @@ class ImageFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<MoveOpEvent> move(
|
@override
|
||||||
|
Stream<MoveOpEvent> move(
|
||||||
Iterable<AvesEntry> entries, {
|
Iterable<AvesEntry> entries, {
|
||||||
@required bool copy,
|
@required bool copy,
|
||||||
@required String destinationAlbum,
|
@required String destinationAlbum,
|
||||||
|
@ -228,7 +313,8 @@ class ImageFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<ExportOpEvent> export(
|
@override
|
||||||
|
Stream<ExportOpEvent> export(
|
||||||
Iterable<AvesEntry> entries, {
|
Iterable<AvesEntry> entries, {
|
||||||
String mimeType = MimeTypes.jpeg,
|
String mimeType = MimeTypes.jpeg,
|
||||||
@required String destinationAlbum,
|
@required String destinationAlbum,
|
||||||
|
@ -246,7 +332,8 @@ class ImageFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map> rename(AvesEntry entry, String newName) async {
|
@override
|
||||||
|
Future<Map> rename(AvesEntry entry, String newName) async {
|
||||||
try {
|
try {
|
||||||
// returns map with: 'contentId' 'path' 'title' 'uri' (all optional)
|
// returns map with: 'contentId' 'path' 'title' 'uri' (all optional)
|
||||||
final result = await platform.invokeMethod('rename', <String, dynamic>{
|
final result = await platform.invokeMethod('rename', <String, dynamic>{
|
||||||
|
@ -260,7 +347,8 @@ class ImageFileService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map> rotate(AvesEntry entry, {@required bool clockwise}) async {
|
@override
|
||||||
|
Future<Map> rotate(AvesEntry entry, {@required bool clockwise}) async {
|
||||||
try {
|
try {
|
||||||
// returns map with: 'rotationDegrees' 'isFlipped'
|
// returns map with: 'rotationDegrees' 'isFlipped'
|
||||||
final result = await platform.invokeMethod('rotate', <String, dynamic>{
|
final result = await platform.invokeMethod('rotate', <String, dynamic>{
|
||||||
|
@ -274,7 +362,8 @@ class ImageFileService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map> flip(AvesEntry entry) async {
|
@override
|
||||||
|
Future<Map> flip(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// returns map with: 'rotationDegrees' 'isFlipped'
|
// returns map with: 'rotationDegrees' 'isFlipped'
|
||||||
final result = await platform.invokeMethod('flip', <String, dynamic>{
|
final result = await platform.invokeMethod('flip', <String, dynamic>{
|
||||||
|
|
|
@ -5,11 +5,21 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:streams_channel/streams_channel.dart';
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
|
|
||||||
class MediaStoreService {
|
abstract class MediaStoreService {
|
||||||
|
Future<List<int>> checkObsoleteContentIds(List<int> knownContentIds);
|
||||||
|
|
||||||
|
Future<List<int>> checkObsoletePaths(Map<int, String> knownPathById);
|
||||||
|
|
||||||
|
// knownEntries: map of contentId -> dateModifiedSecs
|
||||||
|
Stream<AvesEntry> getEntries(Map<int, int> knownEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformMediaStoreService implements MediaStoreService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/mediastore');
|
static const platform = MethodChannel('deckers.thibault/aves/mediastore');
|
||||||
static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/mediastorestream');
|
static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/mediastorestream');
|
||||||
|
|
||||||
static Future<List<int>> checkObsoleteContentIds(List<int> knownContentIds) async {
|
@override
|
||||||
|
Future<List<int>> checkObsoleteContentIds(List<int> knownContentIds) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('checkObsoleteContentIds', <String, dynamic>{
|
final result = await platform.invokeMethod('checkObsoleteContentIds', <String, dynamic>{
|
||||||
'knownContentIds': knownContentIds,
|
'knownContentIds': knownContentIds,
|
||||||
|
@ -21,7 +31,8 @@ class MediaStoreService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<int>> checkObsoletePaths(Map<int, String> knownPathById) async {
|
@override
|
||||||
|
Future<List<int>> checkObsoletePaths(Map<int, String> knownPathById) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('checkObsoletePaths', <String, dynamic>{
|
final result = await platform.invokeMethod('checkObsoletePaths', <String, dynamic>{
|
||||||
'knownPathById': knownPathById,
|
'knownPathById': knownPathById,
|
||||||
|
@ -33,8 +44,8 @@ class MediaStoreService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// knownEntries: map of contentId -> dateModifiedSecs
|
@override
|
||||||
static Stream<AvesEntry> getEntries(Map<int, int> knownEntries) {
|
Stream<AvesEntry> getEntries(Map<int, int> knownEntries) {
|
||||||
try {
|
try {
|
||||||
return _streamChannel.receiveBroadcastStream(<String, dynamic>{
|
return _streamChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
'knownEntries': knownEntries,
|
'knownEntries': knownEntries,
|
||||||
|
|
|
@ -8,11 +8,32 @@ import 'package:aves/services/service_policy.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class MetadataService {
|
abstract class MetadataService {
|
||||||
|
// returns Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
|
||||||
|
Future<Map> getAllMetadata(AvesEntry entry);
|
||||||
|
|
||||||
|
Future<CatalogMetadata> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
||||||
|
|
||||||
|
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry);
|
||||||
|
|
||||||
|
Future<MultiPageInfo> getMultiPageInfo(AvesEntry entry);
|
||||||
|
|
||||||
|
Future<PanoramaInfo> getPanoramaInfo(AvesEntry entry);
|
||||||
|
|
||||||
|
Future<String> getContentResolverProp(AvesEntry entry, String prop);
|
||||||
|
|
||||||
|
Future<List<Uint8List>> getEmbeddedPictures(String uri);
|
||||||
|
|
||||||
|
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry);
|
||||||
|
|
||||||
|
Future<Map> extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformMetadataService implements MetadataService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/metadata');
|
static const platform = MethodChannel('deckers.thibault/aves/metadata');
|
||||||
|
|
||||||
// returns Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
|
@override
|
||||||
static Future<Map> getAllMetadata(AvesEntry entry) async {
|
Future<Map> getAllMetadata(AvesEntry entry) async {
|
||||||
if (entry.isSvg) return null;
|
if (entry.isSvg) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -28,7 +49,8 @@ class MetadataService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<CatalogMetadata> getCatalogMetadata(AvesEntry entry, {bool background = false}) async {
|
@override
|
||||||
|
Future<CatalogMetadata> getCatalogMetadata(AvesEntry entry, {bool background = false}) async {
|
||||||
if (entry.isSvg) return null;
|
if (entry.isSvg) return null;
|
||||||
|
|
||||||
Future<CatalogMetadata> call() async {
|
Future<CatalogMetadata> call() async {
|
||||||
|
@ -65,7 +87,8 @@ class MetadataService {
|
||||||
: call();
|
: call();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry) async {
|
@override
|
||||||
|
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry) async {
|
||||||
if (entry.isSvg) return null;
|
if (entry.isSvg) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -82,7 +105,8 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<MultiPageInfo> getMultiPageInfo(AvesEntry entry) async {
|
@override
|
||||||
|
Future<MultiPageInfo> getMultiPageInfo(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
|
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
|
@ -96,7 +120,8 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PanoramaInfo> getPanoramaInfo(AvesEntry entry) async {
|
@override
|
||||||
|
Future<PanoramaInfo> getPanoramaInfo(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// returns map with values for:
|
// returns map with values for:
|
||||||
// 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int),
|
// 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int),
|
||||||
|
@ -113,7 +138,8 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> getContentResolverProp(AvesEntry entry, String prop) async {
|
@override
|
||||||
|
Future<String> getContentResolverProp(AvesEntry entry, String prop) async {
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('getContentResolverProp', <String, dynamic>{
|
return await platform.invokeMethod('getContentResolverProp', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
|
@ -126,7 +152,8 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
@override
|
||||||
|
Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
|
@ -138,7 +165,8 @@ class MetadataService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async {
|
@override
|
||||||
|
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{
|
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
|
@ -152,7 +180,8 @@ class MetadataService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map> extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async {
|
@override
|
||||||
|
Future<Map> extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
|
final result = await platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
|
|
27
lib/services/services.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:aves/model/availability.dart';
|
||||||
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
import 'package:aves/services/image_file_service.dart';
|
||||||
|
import 'package:aves/services/media_store_service.dart';
|
||||||
|
import 'package:aves/services/metadata_service.dart';
|
||||||
|
import 'package:aves/services/time_service.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
final getIt = GetIt.instance;
|
||||||
|
|
||||||
|
final availability = getIt<AvesAvailability>();
|
||||||
|
final metadataDb = getIt<MetadataDb>();
|
||||||
|
|
||||||
|
final imageFileService = getIt<ImageFileService>();
|
||||||
|
final mediaStoreService = getIt<MediaStoreService>();
|
||||||
|
final metadataService = getIt<MetadataService>();
|
||||||
|
final timeService = getIt<TimeService>();
|
||||||
|
|
||||||
|
void initPlatformServices() {
|
||||||
|
getIt.registerLazySingleton<AvesAvailability>(() => LiveAvesAvailability());
|
||||||
|
getIt.registerLazySingleton<MetadataDb>(() => SqfliteMetadataDb());
|
||||||
|
|
||||||
|
getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService());
|
||||||
|
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService());
|
||||||
|
getIt.registerLazySingleton<MetadataService>(() => PlatformMetadataService());
|
||||||
|
getIt.registerLazySingleton<TimeService>(() => PlatformTimeService());
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/services.dart';
|
||||||
import 'package:aves/utils/string_utils.dart';
|
import 'package:aves/utils/string_utils.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -17,7 +17,7 @@ class SvgMetadataService {
|
||||||
|
|
||||||
static Future<Size> getSize(AvesEntry entry) async {
|
static Future<Size> getSize(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
final data = await ImageFileService.getSvg(entry.uri, entry.mimeType);
|
final data = await imageFileService.getSvg(entry.uri, entry.mimeType);
|
||||||
|
|
||||||
final document = XmlDocument.parse(utf8.decode(data));
|
final document = XmlDocument.parse(utf8.decode(data));
|
||||||
final root = document.rootElement;
|
final root = document.rootElement;
|
||||||
|
@ -59,7 +59,7 @@ class SvgMetadataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final data = await ImageFileService.getSvg(entry.uri, entry.mimeType);
|
final data = await imageFileService.getSvg(entry.uri, entry.mimeType);
|
||||||
|
|
||||||
final document = XmlDocument.parse(utf8.decode(data));
|
final document = XmlDocument.parse(utf8.decode(data));
|
||||||
final root = document.rootElement;
|
final root = document.rootElement;
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class TimeService {
|
abstract class TimeService {
|
||||||
|
Future<String> getDefaultTimeZone();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformTimeService implements TimeService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/time');
|
static const platform = MethodChannel('deckers.thibault/aves/time');
|
||||||
|
|
||||||
static Future<String> getDefaultTimeZone() async {
|
@override
|
||||||
|
Future<String> getDefaultTimeZone() async {
|
||||||
try {
|
try {
|
||||||
return await platform.invokeMethod('getDefaultTimeZone');
|
return await platform.invokeMethod('getDefaultTimeZone');
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'package:flutter/scheduler.dart';
|
||||||
class Durations {
|
class Durations {
|
||||||
// common animations
|
// common animations
|
||||||
static const iconAnimation = Duration(milliseconds: 300);
|
static const iconAnimation = Duration(milliseconds: 300);
|
||||||
static const opToastAnimation = Duration(milliseconds: 600);
|
|
||||||
static const sweeperOpacityAnimation = Duration(milliseconds: 150);
|
static const sweeperOpacityAnimation = Duration(milliseconds: 150);
|
||||||
static const sweepingAnimation = Duration(milliseconds: 650);
|
static const sweepingAnimation = Duration(milliseconds: 650);
|
||||||
static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration
|
static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration
|
||||||
|
@ -43,7 +42,7 @@ class Durations {
|
||||||
static const xmpStructArrayCardTransition = Duration(milliseconds: 300);
|
static const xmpStructArrayCardTransition = Duration(milliseconds: 300);
|
||||||
|
|
||||||
// delays & refresh intervals
|
// delays & refresh intervals
|
||||||
static const opToastDisplay = Duration(seconds: 2);
|
static const opToastDisplay = Duration(seconds: 3);
|
||||||
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
|
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
|
||||||
static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300);
|
static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300);
|
||||||
static const highlightScrollInitDelay = Duration(milliseconds: 800);
|
static const highlightScrollInitDelay = Duration(milliseconds: 800);
|
||||||
|
|
|
@ -43,12 +43,12 @@ class AIcons {
|
||||||
static const IconData openOutside = Icons.open_in_new_outlined;
|
static const IconData openOutside = Icons.open_in_new_outlined;
|
||||||
static const IconData pin = Icons.push_pin_outlined;
|
static const IconData pin = Icons.push_pin_outlined;
|
||||||
static const IconData print = Icons.print_outlined;
|
static const IconData print = Icons.print_outlined;
|
||||||
static const IconData refresh = Icons.refresh_outlined;
|
|
||||||
static const IconData rename = Icons.title_outlined;
|
static const IconData rename = Icons.title_outlined;
|
||||||
static const IconData rotateLeft = Icons.rotate_left_outlined;
|
static const IconData rotateLeft = Icons.rotate_left_outlined;
|
||||||
static const IconData rotateRight = Icons.rotate_right_outlined;
|
static const IconData rotateRight = Icons.rotate_right_outlined;
|
||||||
static const IconData search = Icons.search_outlined;
|
static const IconData search = Icons.search_outlined;
|
||||||
static const IconData select = Icons.select_all_outlined;
|
static const IconData select = Icons.select_all_outlined;
|
||||||
|
static const IconData setCover = MdiIcons.imageEditOutline;
|
||||||
static const IconData share = Icons.share_outlined;
|
static const IconData share = Icons.share_outlined;
|
||||||
static const IconData sort = Icons.sort_outlined;
|
static const IconData sort = Icons.sort_outlined;
|
||||||
static const IconData stats = Icons.pie_chart_outlined;
|
static const IconData stats = Icons.pie_chart_outlined;
|
||||||
|
|
51
lib/theme/themes.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class Themes {
|
||||||
|
static const _accentColor = Colors.indigoAccent;
|
||||||
|
|
||||||
|
static final darkTheme = ThemeData(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
accentColor: _accentColor,
|
||||||
|
scaffoldBackgroundColor: Colors.grey[900],
|
||||||
|
buttonColor: _accentColor,
|
||||||
|
dialogBackgroundColor: Colors.grey[850],
|
||||||
|
toggleableActiveColor: _accentColor,
|
||||||
|
tooltipTheme: TooltipThemeData(
|
||||||
|
verticalOffset: 32,
|
||||||
|
),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
textTheme: TextTheme(
|
||||||
|
headline6: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontFeatures: [FontFeature.enable('smcp')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: Colors.grey[800],
|
||||||
|
contentTextStyle: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
primary: _accentColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
primary: _accentColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
primary: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/android_file_service.dart';
|
import 'package:aves/services/android_file_service.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
@ -115,21 +116,30 @@ class Package {
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class StorageVolume {
|
class StorageVolume {
|
||||||
final String description, path, state;
|
final String _description, path, state;
|
||||||
final bool isPrimary, isRemovable;
|
final bool isPrimary, isRemovable;
|
||||||
|
|
||||||
const StorageVolume({
|
const StorageVolume({
|
||||||
this.description,
|
String description,
|
||||||
this.isPrimary,
|
this.isPrimary,
|
||||||
this.isRemovable,
|
this.isRemovable,
|
||||||
this.path,
|
this.path,
|
||||||
this.state,
|
this.state,
|
||||||
});
|
}) : _description = description;
|
||||||
|
|
||||||
|
String getDescription(BuildContext context) {
|
||||||
|
if (_description != null) return _description;
|
||||||
|
// ideally, the context should always be provided, but in some cases (e.g. album comparison),
|
||||||
|
// this would require numerous additional methods to have the context as argument
|
||||||
|
// for such a minor benefit: fallback volume description on Android < N
|
||||||
|
if (isPrimary) return context?.l10n?.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage';
|
||||||
|
return context?.l10n?.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card';
|
||||||
|
}
|
||||||
|
|
||||||
factory StorageVolume.fromMap(Map map) {
|
factory StorageVolume.fromMap(Map map) {
|
||||||
final isPrimary = map['isPrimary'] ?? false;
|
final isPrimary = map['isPrimary'] ?? false;
|
||||||
return StorageVolume(
|
return StorageVolume(
|
||||||
description: map['description'] ?? (isPrimary ? 'Internal storage' : 'SD card'),
|
description: map['description'],
|
||||||
isPrimary: isPrimary,
|
isPrimary: isPrimary,
|
||||||
isRemovable: map['isRemovable'] ?? false,
|
isRemovable: map['isRemovable'] ?? false,
|
||||||
path: map['path'] ?? '',
|
path: map['path'] ?? '',
|
||||||
|
@ -167,11 +177,9 @@ class VolumeRelativeDirectory {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get directoryDescription => relativeDir.isEmpty ? 'root' : '“$relativeDir”';
|
String getVolumeDescription(BuildContext context) {
|
||||||
|
|
||||||
String get volumeDescription {
|
|
||||||
final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null);
|
final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null);
|
||||||
return volume?.description ?? volumePath;
|
return volume?.getDescription(context) ?? volumePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -21,7 +21,6 @@ class Constants {
|
||||||
);
|
);
|
||||||
|
|
||||||
static const overlayUnknown = '—'; // em dash
|
static const overlayUnknown = '—'; // em dash
|
||||||
static const infoUnknown = 'unknown';
|
|
||||||
|
|
||||||
static final pointNemo = LatLng(-48.876667, -123.393333);
|
static final pointNemo = LatLng(-48.876667, -123.393333);
|
||||||
|
|
||||||
|
@ -66,55 +65,13 @@ class Constants {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
static const List<Dependency> flutterPackages = [
|
static const List<Dependency> flutterPlugins = [
|
||||||
Dependency(
|
|
||||||
name: 'Flutter',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/flutter/flutter/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/flutter/flutter',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Charts',
|
|
||||||
license: 'Apache 2.0',
|
|
||||||
licenseUrl: 'https://github.com/google/charts/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/google/charts',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Collection',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/dart-lang/collection/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/dart-lang/collection',
|
|
||||||
),
|
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Connectivity',
|
name: 'Connectivity',
|
||||||
license: 'BSD 3-Clause',
|
license: 'BSD 3-Clause',
|
||||||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE',
|
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE',
|
||||||
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity',
|
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity',
|
||||||
),
|
),
|
||||||
Dependency(
|
|
||||||
name: 'Country Code',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/denixport/dart.country/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/denixport/dart.country',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Decorated Icon',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Event Bus',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/marcojakob/dart-event-bus/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/marcojakob/dart-event-bus',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Expansion Tile Card',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/Skylled/expansion_tile_card/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/Skylled/expansion_tile_card',
|
|
||||||
),
|
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'FlutterFire (Core, Analytics, Crashlytics)',
|
name: 'FlutterFire (Core, Analytics, Crashlytics)',
|
||||||
license: 'BSD 3-Clause',
|
license: 'BSD 3-Clause',
|
||||||
|
@ -122,10 +79,160 @@ class Constants {
|
||||||
sourceUrl: 'https://github.com/FirebaseExtended/flutterfire',
|
sourceUrl: 'https://github.com/FirebaseExtended/flutterfire',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flushbar',
|
name: 'Flutter ijkplayer',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Google API Availability',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/Baseflow/flutter-google-api-availability/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Google Maps for Flutter',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Package Info',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/package_info/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/package_info',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Permission Handler',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/Baseflow/flutter-permission-handler/blob/develop/permission_handler/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/Baseflow/flutter-permission-handler',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Printing',
|
||||||
license: 'Apache 2.0',
|
license: 'Apache 2.0',
|
||||||
licenseUrl: 'https://github.com/AndreHaueisen/flushbar/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/AndreHaueisen/flushbar',
|
sourceUrl: 'https://github.com/DavBfr/dart_pdf',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Shared Preferences',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'sqflite',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/tekartik/sqflite/blob/master/sqflite/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/tekartik/sqflite',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Streams Channel',
|
||||||
|
license: 'Apache 2.0',
|
||||||
|
licenseUrl: 'https://github.com/loup-v/streams_channel/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/loup-v/streams_channel',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'URL Launcher',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<Dependency> dartPackages = [
|
||||||
|
Dependency(
|
||||||
|
name: 'Collection',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/dart-lang/collection/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/dart-lang/collection',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Country Code',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/denixport/dart.country/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/denixport/dart.country',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Event Bus',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/marcojakob/dart-event-bus/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/marcojakob/dart-event-bus',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Get It',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/fluttercommunity/get_it/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/fluttercommunity/get_it',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Github',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/SpinlockLabs/github.dart/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/SpinlockLabs/github.dart',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Intl',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/dart-lang/intl/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/dart-lang/intl',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'LatLong',
|
||||||
|
license: 'Apache 2.0',
|
||||||
|
licenseUrl: 'https://github.com/MikeMitterer/dart-latlong/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/MikeMitterer/dart-latlong',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'PDF for Dart and Flutter',
|
||||||
|
license: 'Apache 2.0',
|
||||||
|
licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/DavBfr/dart_pdf',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Pedantic',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/dart-lang/pedantic/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/dart-lang/pedantic',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Tuple',
|
||||||
|
license: 'BSD 2-Clause',
|
||||||
|
licenseUrl: 'https://github.com/dart-lang/tuple/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/dart-lang/tuple',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Version',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/dartninja/version/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/dartninja/version',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'XML',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/renggli/dart-xml/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/renggli/dart-xml',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<Dependency> flutterPackages = [
|
||||||
|
Dependency(
|
||||||
|
name: 'Charts',
|
||||||
|
license: 'Apache 2.0',
|
||||||
|
licenseUrl: 'https://github.com/google/charts/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/google/charts',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Decorated Icon',
|
||||||
|
license: 'MIT',
|
||||||
|
licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name: 'Expansion Tile Card',
|
||||||
|
license: 'BSD 3-Clause',
|
||||||
|
licenseUrl: 'https://github.com/Skylled/expansion_tile_card/blob/master/LICENSE',
|
||||||
|
sourceUrl: 'https://github.com/Skylled/expansion_tile_card',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flutter Highlight',
|
name: 'Flutter Highlight',
|
||||||
|
@ -134,10 +241,10 @@ class Constants {
|
||||||
sourceUrl: 'https://github.com/git-touch/highlight',
|
sourceUrl: 'https://github.com/git-touch/highlight',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flutter ijkplayer',
|
name: 'Flutter Localized Locales',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
licenseUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/guidezpl/flutter-localized-locales/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer',
|
sourceUrl: 'https://github.com/guidezpl/flutter-localized-locales',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flutter Map',
|
name: 'Flutter Map',
|
||||||
|
@ -163,36 +270,6 @@ class Constants {
|
||||||
licenseUrl: 'https://github.com/dnfield/flutter_svg/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/dnfield/flutter_svg/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/dnfield/flutter_svg',
|
sourceUrl: 'https://github.com/dnfield/flutter_svg',
|
||||||
),
|
),
|
||||||
Dependency(
|
|
||||||
name: 'Geocoder',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/aloisdeniel/flutter_geocoder/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/aloisdeniel/flutter_geocoder',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Github',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/SpinlockLabs/github.dart/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/SpinlockLabs/github.dart',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Google Maps for Flutter',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Intl',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/dart-lang/intl/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/dart-lang/intl',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'LatLong',
|
|
||||||
license: 'Apache 2.0',
|
|
||||||
licenseUrl: 'https://github.com/MikeMitterer/dart-latlong/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/MikeMitterer/dart-latlong',
|
|
||||||
),
|
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Material Design Icons Flutter',
|
name: 'Material Design Icons Flutter',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
|
@ -205,12 +282,6 @@ class Constants {
|
||||||
licenseUrl: 'https://github.com/boyan01/overlay_support/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/boyan01/overlay_support/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/boyan01/overlay_support',
|
sourceUrl: 'https://github.com/boyan01/overlay_support',
|
||||||
),
|
),
|
||||||
Dependency(
|
|
||||||
name: 'Package Info',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/package_info/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/package_info',
|
|
||||||
),
|
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Palette Generator',
|
name: 'Palette Generator',
|
||||||
license: 'BSD 3-Clause',
|
license: 'BSD 3-Clause',
|
||||||
|
@ -223,84 +294,18 @@ class Constants {
|
||||||
licenseUrl: 'https://github.com/zesage/panorama/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/zesage/panorama/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/zesage/panorama',
|
sourceUrl: 'https://github.com/zesage/panorama',
|
||||||
),
|
),
|
||||||
Dependency(
|
|
||||||
name: 'PDF for Dart and Flutter',
|
|
||||||
license: 'Apache 2.0',
|
|
||||||
licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/DavBfr/dart_pdf',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Pedantic',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/dart-lang/pedantic/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/dart-lang/pedantic',
|
|
||||||
),
|
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Percent Indicator',
|
name: 'Percent Indicator',
|
||||||
license: 'BSD 2-Clause',
|
license: 'BSD 2-Clause',
|
||||||
licenseUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/',
|
sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/',
|
||||||
),
|
),
|
||||||
Dependency(
|
|
||||||
name: 'Permission Handler',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/Baseflow/flutter-permission-handler/blob/develop/permission_handler/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/Baseflow/flutter-permission-handler',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Printing',
|
|
||||||
license: 'Apache 2.0',
|
|
||||||
licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/DavBfr/dart_pdf',
|
|
||||||
),
|
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Provider',
|
name: 'Provider',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
licenseUrl: 'https://github.com/rrousselGit/provider/blob/master/LICENSE',
|
licenseUrl: 'https://github.com/rrousselGit/provider/blob/master/LICENSE',
|
||||||
sourceUrl: 'https://github.com/rrousselGit/provider',
|
sourceUrl: 'https://github.com/rrousselGit/provider',
|
||||||
),
|
),
|
||||||
Dependency(
|
|
||||||
name: 'Shared Preferences',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'sqflite',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/tekartik/sqflite/blob/master/sqflite/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/tekartik/sqflite',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Streams Channel',
|
|
||||||
license: 'Apache 2.0',
|
|
||||||
licenseUrl: 'https://github.com/loup-v/streams_channel/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/loup-v/streams_channel',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Tuple',
|
|
||||||
license: 'BSD 2-Clause',
|
|
||||||
licenseUrl: 'https://github.com/dart-lang/tuple/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/dart-lang/tuple',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'URL Launcher',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'Version',
|
|
||||||
license: 'BSD 3-Clause',
|
|
||||||
licenseUrl: 'https://github.com/dartninja/version/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/dartninja/version',
|
|
||||||
),
|
|
||||||
Dependency(
|
|
||||||
name: 'XML',
|
|
||||||
license: 'MIT',
|
|
||||||
licenseUrl: 'https://github.com/renggli/dart-xml/blob/master/LICENSE',
|
|
||||||
sourceUrl: 'https://github.com/renggli/dart-xml',
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import 'package:aves/widgets/about/app_ref.dart';
|
import 'package:aves/widgets/about/app_ref.dart';
|
||||||
import 'package:aves/widgets/about/credits.dart';
|
import 'package:aves/widgets/about/credits.dart';
|
||||||
import 'package:aves/widgets/about/licenses.dart';
|
import 'package:aves/widgets/about/licenses.dart';
|
||||||
import 'package:aves/widgets/about/new_version.dart';
|
import 'package:aves/widgets/about/update.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AboutPage extends StatelessWidget {
|
class AboutPage extends StatelessWidget {
|
||||||
|
@ -11,7 +12,7 @@ class AboutPage extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('About'),
|
title: Text(context.l10n.aboutPageTitle),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
|
@ -23,7 +24,7 @@ class AboutPage extends StatelessWidget {
|
||||||
[
|
[
|
||||||
AppReference(),
|
AppReference(),
|
||||||
Divider(),
|
Divider(),
|
||||||
AboutNewVersion(),
|
AboutUpdate(),
|
||||||
AboutCredits(),
|
AboutCredits(),
|
||||||
Divider(),
|
Divider(),
|
||||||
],
|
],
|
||||||
|
|