Changes relative to issue#1102
Manga pages now have a proposed name as follow: "MangaName-MangaChapter-MangaPage_yyyy-MM-dd_HHmm.ImageExtension"
This commit is contained in:
@@ -33,10 +33,13 @@ import org.koitharu.kotatsu.core.util.ext.isFileUri
|
|||||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@@ -63,20 +66,20 @@ class PageSaveHelper @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun save(pages: Collection<MangaPage>): Uri? = when (pages.size) {
|
suspend fun save(manga: Manga, chapter: MangaChapter, pageNumber: Int, pages: Set<MangaPage>): Uri? = when (pages.size) {
|
||||||
0 -> null
|
0 -> null
|
||||||
1 -> saveImpl(pages.first())
|
1 -> saveImpl(manga, chapter, pageNumber, pages.first())
|
||||||
else -> {
|
else -> {
|
||||||
saveImpl(pages)
|
saveImpl(manga, chapter, pageNumber, pages)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveImpl(page: MangaPage): Uri {
|
private suspend fun saveImpl(manga: Manga, chapter: MangaChapter, pageNumber: Int, page: MangaPage): Uri {
|
||||||
val pageLoader = pageLoaderProvider.get()
|
val pageLoader = pageLoaderProvider.get()
|
||||||
val pageUrl = pageLoader.getPageUrl(page).toUri()
|
val pageUrl = pageLoader.getPageUrl(page).toUri()
|
||||||
val pageUri = pageLoader.loadPage(page, force = false)
|
val pageUri = pageLoader.loadPage(page, force = false)
|
||||||
val proposedName = getProposedFileName(pageUrl, pageUri)
|
val proposedName = getProposedFileName(manga, chapter, pageNumber, pageUrl, pageUri)
|
||||||
val destination = getDefaultFileUri(proposedName)?.uri ?: run {
|
val destination = getDefaultFileUri(proposedName)?.uri ?: run {
|
||||||
val defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString()
|
val defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString()
|
||||||
savePageRequest.launchAndAwait(defaultUri ?: proposedName)
|
savePageRequest.launchAndAwait(defaultUri ?: proposedName)
|
||||||
@@ -85,25 +88,67 @@ class PageSaveHelper @AssistedInject constructor(
|
|||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveImpl(pages: Collection<MangaPage>) {
|
private suspend fun saveImpl(manga: Manga, chapter: MangaChapter, pageNumber: Int, pages: Collection<MangaPage>) {
|
||||||
val pageLoader = pageLoaderProvider.get()
|
val pageLoader = pageLoaderProvider.get()
|
||||||
val destinationDir = getDefaultFileUri(null) ?: run {
|
val destinationDir = getDefaultFileUri(null) ?: run {
|
||||||
val defaultUri = settings.getPagesSaveDir(context)?.uri
|
val defaultUri = settings.getPagesSaveDir(context)?.uri
|
||||||
DocumentFile.fromTreeUri(context, pickDirectoryRequest.launchAndAwait(defaultUri))
|
DocumentFile.fromTreeUri(context, pickDirectoryRequest.launchAndAwait(defaultUri))
|
||||||
} ?: throw IOException("Cannot get destination directory")
|
} ?: throw IOException("Cannot get destination directory")
|
||||||
|
|
||||||
|
var count = 0
|
||||||
for (page in pages) {
|
for (page in pages) {
|
||||||
val pageUrl = pageLoader.getPageUrl(page).toUri()
|
val pageUrl = pageLoader.getPageUrl(page).toUri()
|
||||||
val pageUri = pageLoader.loadPage(page, force = false)
|
val pageUri = pageLoader.loadPage(page, force = false)
|
||||||
val proposedName = getProposedFileName(pageUrl, pageUri)
|
val proposedName = getProposedFileName(manga, chapter, pageNumber.plus(count), pageUrl, pageUri)
|
||||||
val ext = proposedName.substringAfterLast('.', "")
|
val ext = proposedName.substringAfterLast('.', "")
|
||||||
val mime = requireNotNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)) {
|
val mime = requireNotNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)) {
|
||||||
"Unknown type of $proposedName"
|
"Unknown type of $proposedName"
|
||||||
}
|
}
|
||||||
val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.'))
|
val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.'))
|
||||||
copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file"))
|
copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file"))
|
||||||
|
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getProposedFileName(manga: Manga, chapter: MangaChapter, pageNumber: Number, pageUrl: Uri, pageUri: Uri): String {
|
||||||
|
var mangaInfos = getNameFromMangaChapterPage(manga, chapter, pageNumber)
|
||||||
|
var currentTime = getCurrentTime()
|
||||||
|
var extension = getPageExtension(pageUrl, pageUri)
|
||||||
|
|
||||||
|
return mangaInfos + "_" + currentTime + "_" + extension
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNameFromMangaChapterPage(manga: Manga, chapter: MangaChapter, pageNumber: Number): String {
|
||||||
|
return manga.title + "-" + chapter.number + "-" + pageNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentTime(): String {
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm")
|
||||||
|
val current = LocalDateTime.now().format(formatter)
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getPageExtension(url: Uri, fileUri: Uri): String {
|
||||||
|
var name = requireNotNull(
|
||||||
|
if (url.isZipUri()) {
|
||||||
|
url.fragment?.substringAfterLast(File.separatorChar)
|
||||||
|
} else {
|
||||||
|
url.lastPathSegment
|
||||||
|
},
|
||||||
|
) { "Invalid page url: $url" }
|
||||||
|
var extension = name.substringAfterLast('.', "")
|
||||||
|
if (extension.length !in 2..4) {
|
||||||
|
val mimeType = fileUri.toFileOrNull()?.let { file -> getImageMimeType(file) }
|
||||||
|
extension = if (mimeType != null) {
|
||||||
|
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
||||||
|
} else {
|
||||||
|
EXTENSION_FALLBACK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ".$extension"
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <I> ActivityResultLauncher<I>.launchAndAwait(input: I): Uri {
|
private suspend fun <I> ActivityResultLauncher<I>.launchAndAwait(input: I): Uri {
|
||||||
continuation?.cancel()
|
continuation?.cancel()
|
||||||
return withContext(Dispatchers.Main) {
|
return withContext(Dispatchers.Main) {
|
||||||
@@ -150,27 +195,6 @@ class PageSaveHelper @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getProposedFileName(url: Uri, fileUri: Uri): String {
|
|
||||||
var name = requireNotNull(
|
|
||||||
if (url.isZipUri()) {
|
|
||||||
url.fragment?.substringAfterLast(File.separatorChar)
|
|
||||||
} else {
|
|
||||||
url.lastPathSegment
|
|
||||||
},
|
|
||||||
) { "Invalid page url: $url" }
|
|
||||||
var extension = name.substringAfterLast('.', "")
|
|
||||||
name = name.substringBeforeLast('.')
|
|
||||||
if (extension.length !in 2..4) {
|
|
||||||
val mimeType = fileUri.toFileOrNull()?.let { file -> getImageMimeType(file) }
|
|
||||||
extension = if (mimeType != null) {
|
|
||||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
|
||||||
} else {
|
|
||||||
EXTENSION_FALLBACK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
|
private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
|
||||||
val options = BitmapFactory.Options().apply {
|
val options = BitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
|||||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||||
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
|
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
|
||||||
@@ -257,12 +258,26 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val prevJob = pageSaveJob
|
val prevJob = pageSaveJob
|
||||||
pageSaveJob = launchLoadingJob(Dispatchers.Default) {
|
pageSaveJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
|
val currentManga = checkNotNull(getCurrentManga()) { "Cannot find current manga" }
|
||||||
|
val currentChapter = checkNotNull(getCurrentChapter()) { "Cannot find current chapter" }
|
||||||
|
val currentPageNumber = checkNotNull(getPageNumber()) { "Cannot find current page number" }
|
||||||
val currentPage = checkNotNull(getCurrentPage()) { "Cannot find current page" }
|
val currentPage = checkNotNull(getCurrentPage()) { "Cannot find current page" }
|
||||||
val dest = pageSaveHelper.save(setOf(currentPage))
|
val dest = pageSaveHelper.save(currentManga, currentChapter, currentPageNumber, setOf(currentPage))
|
||||||
onPageSaved.call(dest)
|
onPageSaved.call(dest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCurrentManga(): Manga? {
|
||||||
|
return manga.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentChapter(): MangaChapter? {
|
||||||
|
val state = readingState.value?: return null
|
||||||
|
return manga.value?.chapters?.find {
|
||||||
|
it.id == state.chapterId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getCurrentPage(): MangaPage? {
|
fun getCurrentPage(): MangaPage? {
|
||||||
val state = readingState.value ?: return null
|
val state = readingState.value ?: return null
|
||||||
return content.value.pages.find {
|
return content.value.pages.find {
|
||||||
@@ -270,6 +285,11 @@ class ReaderViewModel @Inject constructor(
|
|||||||
}?.toMangaPage()
|
}?.toMangaPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPageNumber(): Int? {
|
||||||
|
val state = readingState.value?: return null
|
||||||
|
return state.page
|
||||||
|
}
|
||||||
|
|
||||||
fun switchChapter(id: Long, page: Int) {
|
fun switchChapter(id: Long, page: Int) {
|
||||||
val prevJob = loadingJob
|
val prevJob = loadingJob
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
|
|||||||
Reference in New Issue
Block a user