Import local manga

This commit is contained in:
Koitharu
2020-02-23 18:02:55 +02:00
parent 013a734136
commit dce877a139
10 changed files with 200 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
package org.koitharu.kotatsu.core.exceptions
import java.io.IOException
class UnsupportedFileException(message: String? = null) : IOException(message)

View File

@@ -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"
}
}
}

View File

@@ -72,7 +72,7 @@ class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<Man
Snackbar.make(
recyclerView, getString(
R.string._s_removed_from_history,
item.title.ellipsize(14)
item.title.ellipsize(16)
), Snackbar.LENGTH_SHORT
).show()
}

View File

@@ -1,9 +1,19 @@
package org.koitharu.kotatsu.ui.main.list.local
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.ellipsize
import java.io.File
class LocalListFragment : MangaListFragment<File>() {
@@ -16,6 +26,33 @@ class LocalListFragment : MangaListFragment<File>() {
}
}
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<File>() {
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()
}
}

View File

@@ -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<MangaListView<File>>() {
class LocalListPresenter : BasePresenter<MangaListView<File>>() {
private lateinit var repository: LocalMangaRepository
@@ -30,6 +40,7 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
repository.getList(0)
}
viewState.onListChanged(list)
} catch (e: CancellationException) {
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
@@ -40,4 +51,54 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
}
}
}
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()
}
}
}
}
}

View File

@@ -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('/')
}

View File

@@ -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, R> T.safe(action: T.() -> R?) = try {
@@ -32,6 +33,7 @@ suspend inline fun <T, R> 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)

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_import"
android:orderInCategory="50"
android:title="@string/_import"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_delete"
android:title="@string/delete" />
</menu>

View File

@@ -65,8 +65,13 @@
<string name="text_clear_history_prompt">Are you rally want to clear all your reading history? This action cannot be undone.</string>
<string name="remove">Remove</string>
<string name="_s_removed_from_history">\"%s\" removed from history</string>
<string name="_s_deleted_from_local_storage">\"%s\" deleted from local storage</string>
<string name="wait_for_loading_finish">Wait for the load to finish</string>
<string name="save_page">Save page</string>
<string name="page_saved">Page saved successful</string>
<string name="share_image">Share image</string>
<string name="_import">Import</string>
<string name="delete">Delete</string>
<string name="operation_not_supported">This operation is not supported</string>
<string name="text_file_not_supported">Invalid file. Only ZIP and CBZ are supported.</string>
</resources>