Fix pages saving #151
This commit is contained in:
@@ -7,7 +7,6 @@ import org.koitharu.kotatsu.download.domain.DownloadManager
|
|||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.local.ui.LocalListViewModel
|
import org.koitharu.kotatsu.local.ui.LocalListViewModel
|
||||||
import org.koitharu.kotatsu.utils.ExternalStorageHelper
|
|
||||||
|
|
||||||
val localModule
|
val localModule
|
||||||
get() = module {
|
get() = module {
|
||||||
@@ -15,8 +14,6 @@ val localModule
|
|||||||
single { LocalStorageManager(androidContext(), get()) }
|
single { LocalStorageManager(androidContext(), get()) }
|
||||||
single { LocalMangaRepository(get()) }
|
single { LocalMangaRepository(get()) }
|
||||||
|
|
||||||
factory { ExternalStorageHelper(androidContext()) }
|
|
||||||
|
|
||||||
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
|
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
|
||||||
|
|
||||||
viewModel { LocalListViewModel(get(), get(), get(), get()) }
|
viewModel { LocalListViewModel(get(), get(), get(), get()) }
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.reader
|
package org.koitharu.kotatsu.reader
|
||||||
|
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageSaveHelper
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||||
|
|
||||||
val readerModule
|
val readerModule
|
||||||
@@ -12,6 +14,8 @@ val readerModule
|
|||||||
single { MangaDataRepository(get()) }
|
single { MangaDataRepository(get()) }
|
||||||
single { PagesCache(get()) }
|
single { PagesCache(get()) }
|
||||||
|
|
||||||
|
factory { PageSaveHelper(get(), androidContext()) }
|
||||||
|
|
||||||
viewModel { params ->
|
viewModel { params ->
|
||||||
ReaderViewModel(
|
ReaderViewModel(
|
||||||
intent = params[0],
|
intent = params[0],
|
||||||
@@ -21,7 +25,7 @@ val readerModule
|
|||||||
historyRepository = get(),
|
historyRepository = get(),
|
||||||
shortcutsRepository = get(),
|
shortcutsRepository = get(),
|
||||||
settings = get(),
|
settings = get(),
|
||||||
externalStorageHelper = get(),
|
pageSaveHelper = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,10 @@ class PageLoader : KoinComponent, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getPageUrl(page: MangaPage): String {
|
||||||
|
return getRepository(page.source).getPageUrl(page)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onIdle() {
|
private fun onIdle() {
|
||||||
synchronized(prefetchQueue) {
|
synchronized(prefetchQueue) {
|
||||||
while (prefetchQueue.isNotEmpty()) {
|
while (prefetchQueue.isNotEmpty()) {
|
||||||
@@ -151,7 +155,7 @@ class PageLoader : KoinComponent, Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File {
|
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File {
|
||||||
val pageUrl = getRepository(page.source).getPageUrl(page)
|
val pageUrl = getPageUrl(page)
|
||||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
||||||
val uri = Uri.parse(pageUrl)
|
val uri = Uri.parse(pageUrl)
|
||||||
return if (uri.scheme == "cbz") {
|
return if (uri.scheme == "cbz") {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okio.IOException
|
||||||
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
class PageSaveHelper(
|
||||||
|
private val cache: PagesCache,
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var continuation: Continuation<Uri>? = null
|
||||||
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
suspend fun savePage(
|
||||||
|
pageLoader: PageLoader,
|
||||||
|
page: MangaPage,
|
||||||
|
saveLauncher: ActivityResultLauncher<String>,
|
||||||
|
): Uri {
|
||||||
|
var pageFile = cache[page.url]
|
||||||
|
var fileName = pageFile?.name
|
||||||
|
if (fileName == null) {
|
||||||
|
fileName = pageLoader.getPageUrl(page).toHttpUrl().pathSegments.last()
|
||||||
|
}
|
||||||
|
val cc = coroutineContext
|
||||||
|
val destination = suspendCancellableCoroutine<Uri> { cont ->
|
||||||
|
continuation = cont
|
||||||
|
Dispatchers.Main.dispatch(cc) {
|
||||||
|
saveLauncher.launch(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation = null
|
||||||
|
if (pageFile == null) {
|
||||||
|
pageFile = pageLoader.loadPage(page, force = false)
|
||||||
|
}
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
contentResolver.openOutputStream(destination)?.use { output ->
|
||||||
|
pageFile.inputStream().use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
} ?: throw IOException("Output stream is null")
|
||||||
|
}
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
|
||||||
|
resume(uri)
|
||||||
|
} != null
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import android.view.*
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.view.*
|
import androidx.core.view.*
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -187,10 +186,7 @@ class ReaderActivity :
|
|||||||
R.id.action_save_page -> {
|
R.id.action_save_page -> {
|
||||||
viewModel.getCurrentPage()?.also { page ->
|
viewModel.getCurrentPage()?.also { page ->
|
||||||
viewModel.saveCurrentState(reader?.getCurrentState())
|
viewModel.saveCurrentState(reader?.getCurrentState())
|
||||||
val name = page.url.toUri().run {
|
viewModel.saveCurrentPage(page, savePageRequest)
|
||||||
fragment ?: lastPathSegment ?: ""
|
|
||||||
}
|
|
||||||
savePageRequest.launch(name)
|
|
||||||
} ?: showWaitWhileLoading()
|
} ?: showWaitWhileLoading()
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
@@ -199,9 +195,7 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(uri: Uri?) {
|
override fun onActivityResult(uri: Uri?) {
|
||||||
if (uri != null) {
|
viewModel.onActivityResult(uri)
|
||||||
viewModel.saveCurrentPage(uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.LongSparseArray
|
import android.util.LongSparseArray
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -26,7 +27,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
|||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||||
import org.koitharu.kotatsu.utils.ExternalStorageHelper
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.IgnoreErrors
|
import org.koitharu.kotatsu.utils.ext.IgnoreErrors
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
@@ -40,10 +40,11 @@ class ReaderViewModel(
|
|||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val shortcutsRepository: ShortcutsRepository,
|
private val shortcutsRepository: ShortcutsRepository,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val externalStorageHelper: ExternalStorageHelper,
|
private val pageSaveHelper: PageSaveHelper,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
|
private var pageSaveJob: Job? = null
|
||||||
private val currentState = MutableStateFlow(initialState)
|
private val currentState = MutableStateFlow(initialState)
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
private val mangaData = MutableStateFlow(intent.manga)
|
||||||
private val chapters = LongSparseArray<MangaChapter>()
|
private val chapters = LongSparseArray<MangaChapter>()
|
||||||
@@ -137,12 +138,16 @@ class ReaderViewModel(
|
|||||||
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCurrentPage(destination: Uri) {
|
fun saveCurrentPage(
|
||||||
launchJob(Dispatchers.Default) {
|
page: MangaPage,
|
||||||
|
saveLauncher: ActivityResultLauncher<String>,
|
||||||
|
) {
|
||||||
|
val prevJob = pageSaveJob
|
||||||
|
pageSaveJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
|
prevJob?.cancelAndJoin()
|
||||||
try {
|
try {
|
||||||
val page = getCurrentPage() ?: error("Page not found")
|
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
|
||||||
externalStorageHelper.savePage(page, destination)
|
onPageSaved.postCall(dest)
|
||||||
onPageSaved.postCall(destination)
|
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -154,6 +159,15 @@ class ReaderViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onActivityResult(uri: Uri?) {
|
||||||
|
if (uri != null) {
|
||||||
|
pageSaveHelper.onActivityResult(uri)
|
||||||
|
} else {
|
||||||
|
pageSaveJob?.cancel()
|
||||||
|
pageSaveJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getCurrentPage(): MangaPage? {
|
fun getCurrentPage(): MangaPage? {
|
||||||
val state = currentState.value ?: return null
|
val state = currentState.value ?: return null
|
||||||
return content.value?.pages?.find {
|
return content.value?.pages?.find {
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import okio.IOException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
|
|
||||||
class ExternalStorageHelper(context: Context) {
|
|
||||||
|
|
||||||
private val contentResolver = context.contentResolver
|
|
||||||
|
|
||||||
suspend fun savePage(page: MangaPage, destination: Uri) {
|
|
||||||
val pageLoader = PageLoader()
|
|
||||||
val pageFile = pageLoader.loadPage(page, force = false)
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
contentResolver.openOutputStream(destination)?.use { output ->
|
|
||||||
pageFile.inputStream().use { input ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
} ?: throw IOException("Output stream is null")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user