#30 improved obsolete file handling
This commit is contained in:
parent
1533707aa6
commit
4612d2f4fd
8 changed files with 132 additions and 55 deletions
|
@ -13,6 +13,7 @@ import androidx.core.graphics.drawable.IconCompat
|
||||||
import app.loup.streams_channel.StreamsChannel
|
import app.loup.streams_channel.StreamsChannel
|
||||||
import deckers.thibault.aves.channel.calls.*
|
import deckers.thibault.aves.channel.calls.*
|
||||||
import deckers.thibault.aves.channel.streams.*
|
import deckers.thibault.aves.channel.streams.*
|
||||||
|
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.PermissionManager
|
import deckers.thibault.aves.utils.PermissionManager
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
@ -84,21 +85,29 @@ class MainActivity : FlutterActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) {
|
when (requestCode) {
|
||||||
val treeUri = data?.data
|
VOLUME_ACCESS_REQUEST -> {
|
||||||
if (resultCode != RESULT_OK || treeUri == null) {
|
val treeUri = data?.data
|
||||||
PermissionManager.onPermissionResult(requestCode, null)
|
if (resultCode != RESULT_OK || treeUri == null) {
|
||||||
return
|
PermissionManager.onPermissionResult(requestCode, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save access permissions across reboots
|
||||||
|
val takeFlags = (data.flags
|
||||||
|
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
|
||||||
|
contentResolver.takePersistableUriPermission(treeUri, takeFlags)
|
||||||
|
|
||||||
|
// resume pending action
|
||||||
|
PermissionManager.onPermissionResult(requestCode, treeUri)
|
||||||
|
}
|
||||||
|
DELETE_PERMISSION_REQUEST -> {
|
||||||
|
// delete permission may be requested on Android 10+ only
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save access permissions across reboots
|
|
||||||
val takeFlags = (data.flags
|
|
||||||
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
|
|
||||||
contentResolver.takePersistableUriPermission(treeUri, takeFlags)
|
|
||||||
|
|
||||||
// resume pending action
|
|
||||||
PermissionManager.onPermissionResult(requestCode, treeUri)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,5 +183,7 @@ class MainActivity : FlutterActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<MainActivity>()
|
private val LOG_TAG = LogUtils.createTag<MainActivity>()
|
||||||
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
|
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
|
||||||
|
const val VOLUME_ACCESS_REQUEST = 1
|
||||||
|
const val DELETE_PERMISSION_REQUEST = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package deckers.thibault.aves.channel.streams
|
package deckers.thibault.aves.channel.streams
|
||||||
|
|
||||||
import android.content.Context
|
import android.app.Activity
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ImageOpStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler {
|
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
|
||||||
private lateinit var eventSink: EventSink
|
private lateinit var eventSink: EventSink
|
||||||
private lateinit var handler: Handler
|
private lateinit var handler: Handler
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
|
||||||
"uri" to uri.toString(),
|
"uri" to uri.toString(),
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
provider.delete(context, uri, path)
|
provider.delete(activity, uri, path)
|
||||||
result["success"] = true
|
result["success"] = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
|
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
|
||||||
|
@ -138,7 +138,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
|
||||||
|
|
||||||
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
|
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
|
||||||
val entries = entryMapList.map(::AvesEntry)
|
val entries = entryMapList.map(::AvesEntry)
|
||||||
provider.exportMultiple(context, mimeType, destinationDir, entries, object : ImageOpCallback {
|
provider.exportMultiple(activity, mimeType, destinationDir, entries, object : ImageOpCallback {
|
||||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||||
override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable)
|
override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable)
|
||||||
})
|
})
|
||||||
|
@ -168,7 +168,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments:
|
||||||
|
|
||||||
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
|
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
|
||||||
val entries = entryMapList.map(::AvesEntry)
|
val entries = entryMapList.map(::AvesEntry)
|
||||||
provider.moveMultiple(context, copy, destinationDir, entries, object : ImageOpCallback {
|
provider.moveMultiple(activity, copy, destinationDir, entries, object : ImageOpCallback {
|
||||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||||
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
|
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package deckers.thibault.aves.model.provider
|
package deckers.thibault.aves.model.provider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
@ -40,11 +41,11 @@ abstract class ImageProvider {
|
||||||
callback.onFailure(UnsupportedOperationException())
|
callback.onFailure(UnsupportedOperationException())
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun delete(context: Context, uri: Uri, path: String?) {
|
open suspend fun delete(activity: Activity, uri: Uri, path: String?) {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List<AvesEntry>, callback: ImageOpCallback) {
|
open suspend fun moveMultiple(activity: Activity, copy: Boolean, destinationDir: String, entries: List<AvesEntry>, callback: ImageOpCallback) {
|
||||||
callback.onFailure(UnsupportedOperationException())
|
callback.onFailure(UnsupportedOperationException())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package deckers.thibault.aves.model.provider
|
package deckers.thibault.aves.model.provider
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.RecoverableSecurityException
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -8,6 +10,7 @@ import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.commonsware.cwac.document.DocumentFileCompat
|
import com.commonsware.cwac.document.DocumentFileCompat
|
||||||
|
import deckers.thibault.aves.MainActivity.Companion.DELETE_PERMISSION_REQUEST
|
||||||
import deckers.thibault.aves.model.AvesEntry
|
import deckers.thibault.aves.model.AvesEntry
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.model.SourceEntry
|
import deckers.thibault.aves.model.SourceEntry
|
||||||
|
@ -22,6 +25,7 @@ import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission
|
||||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class MediaStoreImageProvider : ImageProvider() {
|
class MediaStoreImageProvider : ImageProvider() {
|
||||||
|
@ -205,31 +209,55 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
||||||
|
|
||||||
// `uri` is a media URI, not a document URI
|
// `uri` is a media URI, not a document URI
|
||||||
override suspend fun delete(context: Context, uri: Uri, path: String?) {
|
override suspend fun delete(activity: Activity, uri: Uri, path: String?) {
|
||||||
path ?: throw Exception("failed to delete file because path is null")
|
path ?: throw Exception("failed to delete file because path is null")
|
||||||
|
|
||||||
if (File(path).exists() && requireAccessPermission(context, path)) {
|
if (File(path).exists() && requireAccessPermission(activity, path)) {
|
||||||
// if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store
|
// if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store
|
||||||
// but it doesn't delete the file, even if the app has the permission
|
// but it doesn't delete the file, even if the app has the permission
|
||||||
val df = getDocumentFile(context, path, uri)
|
val df = getDocumentFile(activity, path, uri)
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
if (df != null && df.delete()) return
|
if (df != null && df.delete()) return
|
||||||
throw Exception("failed to delete file with df=$df")
|
throw Exception("failed to delete file with df=$df")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.contentResolver.delete(uri, null, null) > 0) return
|
try {
|
||||||
|
if (activity.contentResolver.delete(uri, null, null) > 0) return
|
||||||
|
} catch (securityException: SecurityException) {
|
||||||
|
// even if the app has access permission granted on the containing directory,
|
||||||
|
// the delete request may yield a `RecoverableSecurityException` on Android 10+
|
||||||
|
// when the underlying file no longer exists and this is an orphaned entry in the Media Store
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val rse = securityException as? RecoverableSecurityException ?: throw securityException
|
||||||
|
val intentSender = rse.userAction.actionIntent.intentSender
|
||||||
|
|
||||||
|
// request user permission for this item
|
||||||
|
pendingDeleteCompleter = CompletableFuture<Boolean>()
|
||||||
|
activity.startIntentSenderForResult(intentSender, DELETE_PERMISSION_REQUEST, null, 0, 0, 0, null)
|
||||||
|
val granted = pendingDeleteCompleter!!.join()
|
||||||
|
|
||||||
|
pendingDeleteCompleter = null
|
||||||
|
if (granted) {
|
||||||
|
delete(activity, uri, path)
|
||||||
|
} else {
|
||||||
|
throw Exception("failed to get delete permission")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw securityException
|
||||||
|
}
|
||||||
|
}
|
||||||
throw Exception("failed to delete row from content provider")
|
throw Exception("failed to delete row from content provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun moveMultiple(
|
override suspend fun moveMultiple(
|
||||||
context: Context,
|
activity: Activity,
|
||||||
copy: Boolean,
|
copy: Boolean,
|
||||||
destinationDir: String,
|
destinationDir: String,
|
||||||
entries: List<AvesEntry>,
|
entries: List<AvesEntry>,
|
||||||
callback: ImageOpCallback,
|
callback: ImageOpCallback,
|
||||||
) {
|
) {
|
||||||
val destinationDirDocFile = createDirectoryIfAbsent(context, destinationDir)
|
val destinationDirDocFile = createDirectoryIfAbsent(activity, destinationDir)
|
||||||
if (destinationDirDocFile == null) {
|
if (destinationDirDocFile == null) {
|
||||||
callback.onFailure(Exception("failed to create directory at path=$destinationDir"))
|
callback.onFailure(Exception("failed to create directory at path=$destinationDir"))
|
||||||
return
|
return
|
||||||
|
@ -262,7 +290,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
|
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
|
||||||
try {
|
try {
|
||||||
val newFields = moveSingleByTreeDocAndScan(
|
val newFields = moveSingleByTreeDocAndScan(
|
||||||
context, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy,
|
activity, sourcePath, sourceUri, destinationDir, destinationDirDocFile, mimeType, copy,
|
||||||
)
|
)
|
||||||
result["newFields"] = newFields
|
result["newFields"] = newFields
|
||||||
result["success"] = true
|
result["success"] = true
|
||||||
|
@ -275,7 +303,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun moveSingleByTreeDocAndScan(
|
private suspend fun moveSingleByTreeDocAndScan(
|
||||||
context: Context,
|
activity: Activity,
|
||||||
sourcePath: String,
|
sourcePath: String,
|
||||||
sourceUri: Uri,
|
sourceUri: Uri,
|
||||||
destinationDir: String,
|
destinationDir: String,
|
||||||
|
@ -303,12 +331,12 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension)
|
val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension)
|
||||||
val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri)
|
val destinationDocFile = DocumentFileCompat.fromSingleUri(activity, destinationTreeFile.uri)
|
||||||
|
|
||||||
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
||||||
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
|
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
|
||||||
// when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri`
|
// when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri`
|
||||||
val source = DocumentFileCompat.fromSingleUri(context, sourceUri)
|
val source = DocumentFileCompat.fromSingleUri(activity, sourceUri)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
source.copyTo(destinationDocFile)
|
source.copyTo(destinationDocFile)
|
||||||
|
|
||||||
|
@ -322,14 +350,14 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
if (!copy) {
|
if (!copy) {
|
||||||
// delete original entry
|
// delete original entry
|
||||||
try {
|
try {
|
||||||
delete(context, sourceUri, sourcePath)
|
delete(activity, sourceUri, sourcePath)
|
||||||
deletedSource = true
|
deletedSource = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
|
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return scanNewPath(context, destinationFullPath, mimeType).apply {
|
return scanNewPath(activity, destinationFullPath, mimeType).apply {
|
||||||
put("deletedSource", deletedSource)
|
put("deletedSource", deletedSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,6 +394,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
MediaStore.MediaColumns.ORIENTATION,
|
MediaStore.MediaColumns.ORIENTATION,
|
||||||
) else emptyArray()
|
) else emptyArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var pendingDeleteCompleter: CompletableFuture<Boolean>? = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.os.Environment
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import deckers.thibault.aves.MainActivity.Companion.VOLUME_ACCESS_REQUEST
|
||||||
import deckers.thibault.aves.utils.StorageUtils.PathSegments
|
import deckers.thibault.aves.utils.StorageUtils.PathSegments
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -18,8 +19,6 @@ import kotlin.collections.ArrayList
|
||||||
object PermissionManager {
|
object PermissionManager {
|
||||||
private val LOG_TAG = LogUtils.createTag<PermissionManager>()
|
private val LOG_TAG = LogUtils.createTag<PermissionManager>()
|
||||||
|
|
||||||
const val VOLUME_ACCESS_REQUEST_CODE = 1
|
|
||||||
|
|
||||||
// permission request code to pending runnable
|
// permission request code to pending runnable
|
||||||
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
|
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
|
||||||
|
|
||||||
|
@ -39,8 +38,8 @@ object PermissionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent.resolveActivity(activity.packageManager) != null) {
|
if (intent.resolveActivity(activity.packageManager) != null) {
|
||||||
pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied)
|
pendingPermissionMap[VOLUME_ACCESS_REQUEST] = PendingPermissionHandler(path, onGranted, onDenied)
|
||||||
activity.startActivityForResult(intent, VOLUME_ACCESS_REQUEST_CODE, null)
|
activity.startActivityForResult(intent, VOLUME_ACCESS_REQUEST, null)
|
||||||
} else {
|
} else {
|
||||||
Log.e(LOG_TAG, "failed to resolve activity for intent=$intent")
|
Log.e(LOG_TAG, "failed to resolve activity for intent=$intent")
|
||||||
onDenied()
|
onDenied()
|
||||||
|
|
|
@ -8,6 +8,7 @@ class AIcons {
|
||||||
static const IconData vector = Icons.code_outlined;
|
static const IconData vector = Icons.code_outlined;
|
||||||
|
|
||||||
static const IconData android = Icons.android;
|
static const IconData android = Icons.android;
|
||||||
|
static const IconData broken = Icons.broken_image_outlined;
|
||||||
static const IconData checked = Icons.done_outlined;
|
static const IconData checked = Icons.done_outlined;
|
||||||
static const IconData date = Icons.calendar_today_outlined;
|
static const IconData date = Icons.calendar_today_outlined;
|
||||||
static const IconData disc = Icons.fiber_manual_record;
|
static const IconData disc = Icons.fiber_manual_record;
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.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/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ErrorThumbnail extends StatelessWidget {
|
class ErrorThumbnail extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final double extent;
|
final double extent;
|
||||||
final String tooltip;
|
final String tooltip;
|
||||||
|
@ -13,23 +18,53 @@ class ErrorThumbnail extends StatelessWidget {
|
||||||
@required this.tooltip,
|
@required this.tooltip,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ErrorThumbnailState createState() => _ErrorThumbnailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorThumbnailState extends State<ErrorThumbnail> {
|
||||||
|
Future<bool> _exists;
|
||||||
|
|
||||||
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
double get extent => widget.extent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
final color = Colors.blueGrey;
|
||||||
alignment: Alignment.center,
|
return FutureBuilder<bool>(
|
||||||
color: Colors.black,
|
future: _exists,
|
||||||
child: Tooltip(
|
builder: (context, snapshot) {
|
||||||
message: tooltip,
|
if (snapshot.connectionState != ConnectionState.done) return SizedBox();
|
||||||
preferBelow: false,
|
final exists = snapshot.data;
|
||||||
child: Text(
|
return Container(
|
||||||
MimeUtils.displayType(entry.mimeType),
|
alignment: Alignment.center,
|
||||||
style: TextStyle(
|
color: Colors.black,
|
||||||
color: Colors.blueGrey,
|
child: Tooltip(
|
||||||
fontSize: extent / 5,
|
message: exists ? widget.tooltip : context.l10n.viewerErrorDoesNotExist,
|
||||||
),
|
preferBelow: false,
|
||||||
textAlign: TextAlign.center,
|
child: exists
|
||||||
),
|
? Text(
|
||||||
),
|
MimeUtils.displayType(entry.mimeType),
|
||||||
);
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: extent / 5,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
AIcons.broken,
|
||||||
|
size: extent / 2,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ class _ErrorViewState extends State<ErrorView> {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return SizedBox();
|
if (snapshot.connectionState != ConnectionState.done) return SizedBox();
|
||||||
final exists = snapshot.data;
|
final exists = snapshot.data;
|
||||||
return EmptyContent(
|
return EmptyContent(
|
||||||
icon: AIcons.error,
|
icon: exists ? AIcons.error : AIcons.broken,
|
||||||
text: exists ? context.l10n.viewerErrorUnknown : context.l10n.viewerErrorDoesNotExist,
|
text: exists ? context.l10n.viewerErrorUnknown : context.l10n.viewerErrorDoesNotExist,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue