diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt new file mode 100644 index 000000000..50996991f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt @@ -0,0 +1,5 @@ +package org.koitharu.kotatsu.core.exceptions + +import java.io.IOException + +class UnsupportedFileException(message: String? = null) : IOException(message) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt index 6ccd00955..8cce1324d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.safe import java.io.File +import java.util.* import java.util.zip.ZipFile class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaRepository(loaderContext) { @@ -92,6 +93,19 @@ class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaReposit } } + fun delete(manga: Manga): Boolean { + val file = Uri.parse(manga.url).toFile() + return file.delete() + } + private fun zipUri(file: File, entryName: String) = Uri.fromParts("cbz", file.path, entryName).toString() + + companion object { + + fun isFileSupported(name: String): Boolean { + val ext = name.substringAfterLast('.').toLowerCase(Locale.ROOT) + return ext == "cbz" || ext == "zip" + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/history/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/history/HistoryListFragment.kt index 16b619772..3849565bf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/history/HistoryListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/history/HistoryListFragment.kt @@ -72,7 +72,7 @@ class HistoryListFragment : MangaListFragment(), MangaListView() { @@ -16,6 +26,33 @@ class LocalListFragment : MangaListFragment() { } } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.opt_local, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_import -> { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + try { + startActivityForResult(intent, REQUEST_IMPORT) + } catch (e: ActivityNotFoundException) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + Snackbar.make( + recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT + ).show() + } + true + } + else -> super.onOptionsItemSelected(item) + } + } + override fun getTitle(): CharSequence? { return getString(R.string.local_storage) } @@ -25,8 +62,45 @@ class LocalListFragment : MangaListFragment() { textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + REQUEST_IMPORT -> if (resultCode == Activity.RESULT_OK) { + val uri = data?.data ?: return + presenter.importFile(context?.applicationContext ?: return, uri) + } + } + } + + override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) { + super.onCreatePopupMenu(inflater, menu, data) + inflater.inflate(R.menu.popup_local, menu) + } + + override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean { + return when (item.itemId) { + R.id.action_delete -> { + presenter.delete(data) + true + } + else -> super.onPopupMenuItemSelected(item, data) + } + } + + override fun onItemRemoved(item: Manga) { + super.onItemRemoved(item) + Snackbar.make( + recyclerView, getString( + R.string._s_deleted_from_local_storage, + item.title.ellipsize(16) + ), Snackbar.LENGTH_SHORT + ).show() + } + companion object { + private const val REQUEST_IMPORT = 50 + fun newInstance() = LocalListFragment() } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt index a93cdd82b..2f29850b3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt @@ -1,19 +1,29 @@ package org.koitharu.kotatsu.ui.main.list.local +import android.content.Context +import android.net.Uri +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import moxy.InjectViewState import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException +import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.domain.MangaProviderFactory +import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.main.list.MangaListView +import org.koitharu.kotatsu.utils.MediaStoreCompat +import org.koitharu.kotatsu.utils.ext.safe +import org.koitharu.kotatsu.utils.ext.sub import java.io.File +import java.io.IOException @InjectViewState -class LocalListPresenter : BasePresenter>() { +class LocalListPresenter : BasePresenter>() { private lateinit var repository: LocalMangaRepository @@ -30,6 +40,7 @@ class LocalListPresenter : BasePresenter>() { repository.getList(0) } viewState.onListChanged(list) + } catch (e: CancellationException) { } catch (e: Exception) { if (BuildConfig.DEBUG) { e.printStackTrace() @@ -40,4 +51,54 @@ class LocalListPresenter : BasePresenter>() { } } } + + fun importFile(context: Context, uri: Uri) { + launch(Dispatchers.IO) { + try { + val name = MediaStoreCompat.getName(context, uri) + ?: throw IOException("Cannot fetch name from uri: $uri") + if (!LocalMangaRepository.isFileSupported(name)) { + throw UnsupportedFileException("Unsupported file on $uri") + } + val dest = context.getExternalFilesDir("manga")?.sub(name) + ?: throw IOException("External files dir unavailable") + context.contentResolver.openInputStream(uri)?.use { source -> + dest.outputStream().use { output -> + source.copyTo(output) + } + } ?: throw IOException("Cannot open input stream: $uri") + val list = repository.getList(0) + withContext(Dispatchers.Main) { + viewState.onListChanged(list) + } + } catch (e: CancellationException) { + } catch (e: Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + withContext(Dispatchers.Main) { + viewState.onError(e) + } + } + } + } + + fun delete(manga: Manga) { + launch { + try { + withContext(Dispatchers.IO) { + repository.delete(manga) || throw IOException("Unable to delete file") + safe { + HistoryRepository().delete(manga) + } + } + viewState.onItemRemoved(manga) + } catch (e: CancellationException) { + } catch (e: Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MediaStoreCompat.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MediaStoreCompat.kt index b086f039b..c21ef04f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/MediaStoreCompat.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/MediaStoreCompat.kt @@ -2,10 +2,13 @@ package org.koitharu.kotatsu.utils import android.content.ContentResolver import android.content.ContentValues +import android.content.Context import android.net.Uri import android.os.Build import android.provider.MediaStore +import android.provider.OpenableColumns import android.webkit.MimeTypeMap +import androidx.core.database.getStringOrNull import org.koitharu.kotatsu.BuildConfig import java.io.OutputStream @@ -51,4 +54,17 @@ object MediaStoreCompat { } return uri } + + @JvmStatic + fun getName(context: Context, uri: Uri): String? = (if (uri.scheme == "content") { + context.contentResolver.query(uri, null, null, null, null)?.use { + if (it.moveToFirst()) { + it.getStringOrNull(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } else { + null + } + } + } else { + null + }) ?: uri.path?.substringAfterLast('/') } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt index 3d2f20ea5..a801bdcc4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt @@ -4,6 +4,7 @@ import android.content.res.Resources import kotlinx.coroutines.delay import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import java.io.IOException inline fun T.safe(action: T.() -> R?) = try { @@ -32,6 +33,7 @@ suspend inline fun T.retryUntilSuccess(maxAttempts: Int, action: T.() -> } fun Throwable.getDisplayMessage(resources: Resources) = when (this) { + is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is IOException -> resources.getString(R.string.network_error) else -> if (BuildConfig.DEBUG) { message ?: resources.getString(R.string.error_occurred) diff --git a/app/src/main/res/menu/opt_local.xml b/app/src/main/res/menu/opt_local.xml new file mode 100644 index 000000000..517cb28f9 --- /dev/null +++ b/app/src/main/res/menu/opt_local.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popup_local.xml b/app/src/main/res/menu/popup_local.xml new file mode 100644 index 000000000..4880a25a5 --- /dev/null +++ b/app/src/main/res/menu/popup_local.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cff160f61..b1a17dfb1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -65,8 +65,13 @@ Are you rally want to clear all your reading history? This action cannot be undone. Remove \"%s\" removed from history + \"%s\" deleted from local storage Wait for the load to finish Save page Page saved successful Share image + Import + Delete + This operation is not supported + Invalid file. Only ZIP and CBZ are supported. \ No newline at end of file