From 4f22e29ad6fd8d18f55234550bf0ee5dd8cd804d Mon Sep 17 00:00:00 2001 From: Claudio Riccio Date: Wed, 30 Oct 2024 17:56:39 +0100 Subject: [PATCH] Changes relative to issue#1102 Manga pages now have a proposed name as follow: "MangaName-MangaChapter-MangaPage_yyyy-MM-dd_HHmm.ImageExtension" --- .../kotatsu/reader/ui/PageSaveHelper.kt | 82 ++++++++++++------- .../kotatsu/reader/ui/ReaderViewModel.kt | 22 ++++- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt index 755f183c5..00f9c087f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -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.toFileOrNull 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.util.toFileNameSafe import org.koitharu.kotatsu.reader.domain.PageLoader import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import javax.inject.Provider import kotlin.coroutines.resume @@ -63,20 +66,20 @@ class PageSaveHelper @AssistedInject constructor( } } - suspend fun save(pages: Collection): Uri? = when (pages.size) { + suspend fun save(manga: Manga, chapter: MangaChapter, pageNumber: Int, pages: Set): Uri? = when (pages.size) { 0 -> null - 1 -> saveImpl(pages.first()) + 1 -> saveImpl(manga, chapter, pageNumber, pages.first()) else -> { - saveImpl(pages) + saveImpl(manga, chapter, pageNumber, pages) 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 pageUrl = pageLoader.getPageUrl(page).toUri() 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 defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString() savePageRequest.launchAndAwait(defaultUri ?: proposedName) @@ -85,25 +88,67 @@ class PageSaveHelper @AssistedInject constructor( return destination } - private suspend fun saveImpl(pages: Collection) { + private suspend fun saveImpl(manga: Manga, chapter: MangaChapter, pageNumber: Int, pages: Collection) { val pageLoader = pageLoaderProvider.get() val destinationDir = getDefaultFileUri(null) ?: run { val defaultUri = settings.getPagesSaveDir(context)?.uri DocumentFile.fromTreeUri(context, pickDirectoryRequest.launchAndAwait(defaultUri)) } ?: throw IOException("Cannot get destination directory") + + var count = 0 for (page in pages) { val pageUrl = pageLoader.getPageUrl(page).toUri() 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 mime = requireNotNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)) { "Unknown type of $proposedName" } val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.')) 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 ActivityResultLauncher.launchAndAwait(input: I): Uri { continuation?.cancel() 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) { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index d6a411358..4bc3f089e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -57,6 +57,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase import org.koitharu.kotatsu.local.domain.model.LocalManga 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.reader.domain.ChaptersLoader import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase @@ -257,12 +258,26 @@ class ReaderViewModel @Inject constructor( val prevJob = pageSaveJob pageSaveJob = launchLoadingJob(Dispatchers.Default) { 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 dest = pageSaveHelper.save(setOf(currentPage)) + val dest = pageSaveHelper.save(currentManga, currentChapter, currentPageNumber, setOf(currentPage)) 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? { val state = readingState.value ?: return null return content.value.pages.find { @@ -270,6 +285,11 @@ class ReaderViewModel @Inject constructor( }?.toMangaPage() } + fun getPageNumber(): Int? { + val state = readingState.value?: return null + return state.page + } + fun switchChapter(id: Long, page: Int) { val prevJob = loadingJob loadingJob = launchLoadingJob(Dispatchers.Default) {