view mode: fixed opening SVG, fixed deleting items scanned as file content
This commit is contained in:
parent
ba1175e344
commit
2ab0eaeae1
9 changed files with 79 additions and 52 deletions
|
@ -252,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
|
||||
private fun getShareableUri(uri: Uri): Uri? {
|
||||
return when (uri.scheme?.toLowerCase(Locale.ROOT)) {
|
||||
return when (uri.scheme?.lowercase(Locale.ROOT)) {
|
||||
ContentResolver.SCHEME_FILE -> {
|
||||
uri.path?.let { path ->
|
||||
val authority = "${context.applicationContext.packageName}.fileprovider"
|
||||
|
|
|
@ -116,6 +116,16 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
|
||||
val metadataMap = getContentResolverMetadataForUri(contentUri) ?: getContentResolverMetadataForUri(uri)
|
||||
if (metadataMap != null) {
|
||||
result.success(metadataMap)
|
||||
} else {
|
||||
result.error("getContentResolverMetadata-null", "failed to get cursor for contentUri=$contentUri", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentResolverMetadataForUri(contentUri: Uri): FieldMap? {
|
||||
val cursor = context.contentResolver.query(contentUri, null, null, null, null)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val metadataMap = HashMap<String, Any?>()
|
||||
|
@ -137,10 +147,9 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
}
|
||||
cursor.close()
|
||||
result.success(metadataMap)
|
||||
} else {
|
||||
result.error("getContentResolverMetadata-null", "failed to get cursor for contentUri=$contentUri", null)
|
||||
return metadataMap
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
|
|
@ -73,7 +73,7 @@ object Metadata {
|
|||
var timeZone: TimeZone? = null
|
||||
val timeZoneMatcher = VIDEO_TIMEZONE_PATTERN.matcher(dateString)
|
||||
if (timeZoneMatcher.find()) {
|
||||
timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z".toRegex(), "")}")
|
||||
timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z", "")}")
|
||||
dateString = timeZoneMatcher.replaceAll("")
|
||||
}
|
||||
|
||||
|
|
|
@ -436,52 +436,58 @@ abstract class ImageProvider {
|
|||
protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
|
||||
suspendCoroutine { cont ->
|
||||
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
|
||||
var contentId: Long? = null
|
||||
var contentUri: Uri? = null
|
||||
if (newUri != null) {
|
||||
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
||||
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
||||
contentId = newUri.tryParseId()
|
||||
if (contentId != null) {
|
||||
if (isImage(mimeType)) {
|
||||
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
|
||||
} else if (isVideo(mimeType)) {
|
||||
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
|
||||
fun scanUri(uri: Uri?): FieldMap? {
|
||||
uri ?: return null
|
||||
|
||||
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.TITLE,
|
||||
)
|
||||
try {
|
||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val newFields = HashMap<String, Any?>()
|
||||
newFields["uri"] = uri.toString()
|
||||
newFields["contentId"] = uri.tryParseId()
|
||||
newFields["path"] = path
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
|
||||
cursor.close()
|
||||
return newFields
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to scan uri=$uri", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (contentUri == null) {
|
||||
cont.resumeWithException(Exception("failed to get content URI of item at path=$path"))
|
||||
|
||||
if (newUri == null) {
|
||||
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
|
||||
return@scanFile
|
||||
}
|
||||
|
||||
val newFields = HashMap<String, Any?>()
|
||||
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.TITLE,
|
||||
)
|
||||
try {
|
||||
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
newFields["uri"] = contentUri.toString()
|
||||
newFields["contentId"] = contentId
|
||||
newFields["path"] = path
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
|
||||
cursor.close()
|
||||
var contentUri: Uri? = null
|
||||
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
||||
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
||||
val contentId = newUri.tryParseId()
|
||||
if (contentId != null) {
|
||||
if (isImage(mimeType)) {
|
||||
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
|
||||
} else if (isVideo(mimeType)) {
|
||||
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cont.resumeWithException(e)
|
||||
return@scanFile
|
||||
}
|
||||
|
||||
if (newFields.isEmpty()) {
|
||||
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
|
||||
} else {
|
||||
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
|
||||
val newFields = scanUri(contentUri) ?: scanUri(newUri)
|
||||
|
||||
if (newFields != null) {
|
||||
cont.resume(newFields)
|
||||
} else {
|
||||
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ import java.util.*
|
|||
|
||||
object ImageProviderFactory {
|
||||
fun getProvider(uri: Uri): ImageProvider? {
|
||||
return when (uri.scheme?.toLowerCase(Locale.ROOT)) {
|
||||
return when (uri.scheme?.lowercase(Locale.ROOT)) {
|
||||
ContentResolver.SCHEME_CONTENT -> {
|
||||
// a URI's authority is [userinfo@]host[:port]
|
||||
// but we only want the host when comparing to Media Store's "authority"
|
||||
return when (uri.host?.toLowerCase(Locale.ROOT)) {
|
||||
return when (uri.host?.lowercase(Locale.ROOT)) {
|
||||
MediaStore.AUTHORITY -> MediaStoreImageProvider()
|
||||
else -> ContentImageProvider()
|
||||
}
|
||||
|
|
|
@ -300,8 +300,17 @@ object StorageUtils {
|
|||
Log.w(LOG_TAG, "failed to get document URI for mediaUri=$mediaUri", e)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback for older APIs
|
||||
return getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) }
|
||||
val df = getVolumePath(context, anyPath)?.let { convertDirPathToTreeUri(context, it) }?.let { getDocumentFileFromVolumeTree(context, it, anyPath) }
|
||||
if (df != null) return df
|
||||
|
||||
// try to strip user info, if any
|
||||
if (mediaUri.userInfo != null) {
|
||||
val genericMediaUri = Uri.parse(mediaUri.toString().replaceFirst("${mediaUri.userInfo}@", ""))
|
||||
Log.d(LOG_TAG, "retry getDocumentFile for mediaUri=$mediaUri without userInfo: $genericMediaUri")
|
||||
return getDocumentFile(context, anyPath, genericMediaUri)
|
||||
}
|
||||
}
|
||||
// good old `File`
|
||||
return DocumentFileCompat.fromFile(File(anyPath))
|
||||
|
|
|
@ -418,7 +418,7 @@ class AvesEntry {
|
|||
addressDetails = null;
|
||||
}
|
||||
|
||||
Future<void> catalog({bool background = false}) async {
|
||||
Future<void> catalog({bool background = false, bool persist = true}) async {
|
||||
if (isCatalogued) return;
|
||||
if (isSvg) {
|
||||
// vector image sizing is not essential, so we should not spend time for it during loading
|
||||
|
@ -428,7 +428,7 @@ class AvesEntry {
|
|||
await _applyNewFields({
|
||||
'width': size.width.round(),
|
||||
'height': size.height.round(),
|
||||
});
|
||||
}, persist: persist);
|
||||
}
|
||||
catalogMetadata = CatalogMetadata(contentId: contentId);
|
||||
} else {
|
||||
|
@ -538,7 +538,7 @@ class AvesEntry {
|
|||
_addressDetails?.locality,
|
||||
}.any((s) => s != null && s.toUpperCase().contains(query));
|
||||
|
||||
Future<void> _applyNewFields(Map newFields) async {
|
||||
Future<void> _applyNewFields(Map newFields, {required bool persist}) async {
|
||||
final uri = newFields['uri'];
|
||||
if (uri is String) this.uri = uri;
|
||||
final path = newFields['path'];
|
||||
|
@ -560,8 +560,10 @@ class AvesEntry {
|
|||
final isFlipped = newFields['isFlipped'];
|
||||
if (isFlipped is bool) this.isFlipped = isFlipped;
|
||||
|
||||
await metadataDb.saveEntries({this});
|
||||
if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!});
|
||||
if (persist) {
|
||||
await metadataDb.saveEntries({this});
|
||||
if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!});
|
||||
}
|
||||
|
||||
metadataChangeNotifier.notifyListeners();
|
||||
}
|
||||
|
@ -573,7 +575,7 @@ class AvesEntry {
|
|||
final oldDateModifiedSecs = dateModifiedSecs;
|
||||
final oldRotationDegrees = rotationDegrees;
|
||||
final oldIsFlipped = isFlipped;
|
||||
await _applyNewFields(newFields);
|
||||
await _applyNewFields(newFields, persist: true);
|
||||
await _onImageChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
return true;
|
||||
}
|
||||
|
@ -585,7 +587,7 @@ class AvesEntry {
|
|||
final oldDateModifiedSecs = dateModifiedSecs;
|
||||
final oldRotationDegrees = rotationDegrees;
|
||||
final oldIsFlipped = isFlipped;
|
||||
await _applyNewFields(newFields);
|
||||
await _applyNewFields(newFields, persist: true);
|
||||
await _onImageChanged(oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ class _HomePageState extends State<HomePage> {
|
|||
final entry = await imageFileService.getEntry(uri, mimeType);
|
||||
if (entry != null) {
|
||||
// cataloguing is essential for coordinates and video rotation
|
||||
await entry.catalog();
|
||||
await entry.catalog(background: false, persist: false);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
|
|
@ -187,6 +187,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
final selectionCount = selection.length;
|
||||
showOpReport<ExportOpEvent>(
|
||||
context: context,
|
||||
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
|
||||
opStream: imageFileService.export(
|
||||
selection,
|
||||
mimeType: MimeTypes.jpeg,
|
||||
|
|
Loading…
Reference in a new issue