Refactor pages saving

master
Koitharu 2 years ago
parent 02073f6d45
commit 57d1f54318
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -72,7 +72,7 @@ android {
} }
lint { lint {
abortOnError true abortOnError true
disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled' disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled', 'SimpleDateFormat'
} }
testOptions { testOptions {
unitTests.includeAndroidResources true unitTests.includeAndroidResources true

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
@ -34,7 +33,6 @@ class BackupZipOutput(val file: File) : Closeable {
companion object { companion object {
const val DIR_BACKUPS = "backups" const val DIR_BACKUPS = "backups"
@SuppressLint("SimpleDateFormat")
private val dateTimeFormat = SimpleDateFormat("yyyyMMdd-HHmm") private val dateTimeFormat = SimpleDateFormat("yyyyMMdd-HHmm")
fun generateFileName(context: Context) = buildString { fun generateFileName(context: Context) = buildString {

@ -36,10 +36,11 @@ import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
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.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.text.SimpleDateFormat
import java.time.format.DateTimeFormatter import java.util.Date
import javax.inject.Provider import javax.inject.Provider
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -66,20 +67,20 @@ class PageSaveHelper @AssistedInject constructor(
} }
} }
suspend fun save(manga: Manga, chapter: MangaChapter, pageNumber: Int, pages: Set<MangaPage>): Uri? = when (pages.size) { suspend fun save(tasks: Set<Task>): Uri? = when (tasks.size) {
0 -> null 0 -> null
1 -> saveImpl(manga, chapter, pageNumber, pages.first()) 1 -> saveImpl(tasks.first())
else -> { else -> {
saveImpl(manga, chapter, pageNumber, pages) saveImpl(tasks)
null null
} }
} }
private suspend fun saveImpl(manga: Manga, chapter: MangaChapter, pageNumber: Int, page: MangaPage): Uri { private suspend fun saveImpl(task: Task): Uri {
val pageLoader = pageLoaderProvider.get() val pageLoader = pageLoaderProvider.get()
val pageUrl = pageLoader.getPageUrl(page).toUri() val pageUrl = pageLoader.getPageUrl(task.page).toUri()
val pageUri = pageLoader.loadPage(page, force = false) val pageUri = pageLoader.loadPage(task.page, force = false)
val proposedName = getProposedFileName(manga, chapter, pageNumber, pageUrl, pageUri) val proposedName = task.getFileBaseName() + "." + getPageExtension(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)
@ -88,49 +89,28 @@ class PageSaveHelper @AssistedInject constructor(
return destination return destination
} }
private suspend fun saveImpl(manga: Manga, chapter: MangaChapter, pageNumber: Int, pages: Collection<MangaPage>) { private suspend fun saveImpl(tasks: Collection<Task>) {
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 (task in tasks) {
for (page in pages) { val pageUrl = pageLoader.getPageUrl(task.page).toUri()
val pageUrl = pageLoader.getPageUrl(page).toUri() val pageUri = pageLoader.loadPage(task.page, force = false)
val pageUri = pageLoader.loadPage(page, force = false) val proposedName = task.getFileBaseName()
val proposedName = getProposedFileName(manga, chapter, pageNumber.plus(count), pageUrl, pageUri) val ext = getPageExtension(pageUrl, pageUri)
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 { private suspend fun getPageExtension(url: Uri, fileUri: Uri): String {
var name = requireNotNull( val name = requireNotNull(
if (url.isZipUri()) { if (url.isZipUri()) {
url.fragment?.substringAfterLast(File.separatorChar) url.fragment?.substringAfterLast(File.separatorChar)
} else { } else {
@ -146,7 +126,7 @@ class PageSaveHelper @AssistedInject constructor(
EXTENSION_FALLBACK EXTENSION_FALLBACK
} }
} }
return ".$extension" return extension
} }
private suspend fun <I> ActivityResultLauncher<I>.launchAndAwait(input: I): Uri { private suspend fun <I> ActivityResultLauncher<I>.launchAndAwait(input: I): Uri {
@ -203,6 +183,24 @@ class PageSaveHelper @AssistedInject constructor(
options.outMimeType options.outMimeType
} }
data class Task(
val manga: Manga,
val chapter: MangaChapter,
val pageNumber: Int,
val page: MangaPage,
) {
fun getFileBaseName() = buildString {
append(manga.title.toFileNameSafe().take(MAX_BASENAME_LENGTH))
append('-')
append(chapter.number)
append('-')
append(pageNumber)
append('_')
append(SimpleDateFormat("yyyy-MM-dd_HHmm").format(Date()))
}
}
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -211,7 +209,7 @@ class PageSaveHelper @AssistedInject constructor(
private companion object { private companion object {
private const val MAX_FILENAME_LENGTH = 16 private const val MAX_BASENAME_LENGTH = 12
private const val EXTENSION_FALLBACK = "png" private const val EXTENSION_FALLBACK = "png"
} }
} }

@ -57,7 +57,6 @@ 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
@ -260,25 +259,17 @@ class ReaderViewModel @Inject constructor(
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
val state = checkNotNull(getCurrentState()) val state = checkNotNull(getCurrentState())
val currentManga = manga.requireValue() val currentManga = manga.requireValue()
val currentChapter = checkNotNull(currentManga.findChapter(state.chapterId)) val task = PageSaveHelper.Task(
val currentPageNumber = state.page manga = currentManga,
val currentPage = checkNotNull(getCurrentPage()) { "Cannot find current page" } chapter = checkNotNull(currentManga.findChapter(state.chapterId)),
val dest = pageSaveHelper.save(currentManga, currentChapter, currentPageNumber, setOf(currentPage)) pageNumber = state.page,
page = checkNotNull(getCurrentPage()) { "Cannot find current page" },
)
val dest = pageSaveHelper.save(setOf(task))
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 {
@ -286,11 +277,6 @@ 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) {

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> <monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Loading…
Cancel
Save