+image viewer, better dupes protection and background processing

This commit is contained in:
sergeych committed 2026-05-17 12:22:46 +03:00
1 parent 14c9863a83
commit ade4fb1896
18 files changed
+817 -127

No files matched your search

@@ -10,15 +10,26 @@ import net.sergeych.toread.storage.ReadingStateRecord
import net.sergeych.toread.storage.jdbc.H2LibraryDatabase
import net.sergeych.toread.storage.jdbc.LibraryScanner
import org.jetbrains.skia.Image
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable
import java.awt.datatransfer.UnsupportedFlavorException
import java.io.File
import java.io.ByteArrayInputStream
import javax.imageio.ImageIO
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.TimeUnit
import javax.swing.JFileChooser
import kotlin.concurrent.thread
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
actual fun loadDefaultBookBytes(): ByteArray? {
@@ -37,6 +48,22 @@ actual fun decodeBookImage(binary: Fb2Binary): ImageBitmap? =
actual fun decodeImageBytes(bytes: ByteArray): ImageBitmap? =
runCatching { Image.makeFromEncoded(bytes).toComposeImageBitmap() }.getOrNull()
actual suspend fun copyImageToClipboard(bytes: ByteArray, mimeType: String, label: String): Boolean = withContext(Dispatchers.IO) {
runCatching {
val image = ImageIO.read(ByteArrayInputStream(bytes)) ?: return@withContext false
val transferable = object : Transferable {
override fun getTransferDataFlavors(): Array<DataFlavor> = arrayOf(DataFlavor.imageFlavor)
override fun isDataFlavorSupported(flavor: DataFlavor): Boolean = flavor == DataFlavor.imageFlavor
override fun getTransferData(flavor: DataFlavor): Any {
if (!isDataFlavorSupported(flavor)) throw UnsupportedFlavorException(flavor)
return image
}
}
Toolkit.getDefaultToolkit().systemClipboard.setContents(transferable, null)
true
}.getOrDefault(false)
}
actual fun defaultLibraryScanPath(): String? = findProjectRoot()?.let { File(it, "test_books").absolutePath }
actual suspend fun loadPlatformOpenBookRequest(): PlatformOpenBookRequest? = null
@@ -79,27 +106,53 @@ actual suspend fun loadLibraryItemCover(fileId: String): LibraryCover? = withCon
actual suspend fun scanLibrarySubtree(
path: String,
onProgress: (LibraryScanProgress) -> Unit,
): LibraryScanReport = withContext(Dispatchers.IO) {
appendLibraryLog("scan requested path=$path")
openLibraryDatabase().useLibrary { db ->
val summary = LibraryScanner(db, ::appendLibraryLog).scanSubtree(File(path)) {
onProgress(
LibraryScanProgress(
scannedFiles = it.scannedFiles,
importedFiles = it.importedFiles,
skippedFiles = it.skippedFiles,
failedFiles = it.failedFiles,
currentFile = it.currentFile,
): LibraryScanReport = coroutineScope {
withContext(Dispatchers.IO) {
appendLibraryLog("scan requested path=$path")
}
val root = File(path)
val totalFiles = AtomicInteger(-1)
val latestProgress = AtomicReference<LibraryScanProgress?>(null)
fun emitProgress(progress: LibraryScanProgress) {
val enriched = totalFiles.get().takeIf { it >= 0 }?.let { progress.copy(totalFiles = it) } ?: progress
latestProgress.set(enriched)
onProgress(enriched)
}
val totalJob = async(Dispatchers.IO) {
runCatching { LibraryScanner.countSupportedBookFiles(root) }.getOrNull()
}
val totalPublisher = launch(Dispatchers.IO) {
val total = totalJob.await() ?: return@launch
totalFiles.set(total)
emitProgress(latestProgress.get()?.copy(totalFiles = total) ?: LibraryScanProgress(0, 0, 0, 0, totalFiles = total))
}
val report = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
val summary = LibraryScanner(db, ::appendLibraryLog).scanSubtree(root) {
emitProgress(
LibraryScanProgress(
scannedFiles = it.scannedFiles,
importedFiles = it.importedFiles,
skippedFiles = it.skippedFiles,
failedFiles = it.failedFiles,
currentFile = it.currentFile,
)
)
}
LibraryScanReport(
scannedFiles = summary.scannedFiles,
importedFiles = summary.importedFiles,
skippedFiles = summary.skippedFiles,
failedFiles = summary.failedFiles,
totalFiles = totalFiles.get().takeIf { it >= 0 },
)
}
LibraryScanReport(
scannedFiles = summary.scannedFiles,
importedFiles = summary.importedFiles,
skippedFiles = summary.skippedFiles,
failedFiles = summary.failedFiles,
)
}
totalPublisher.join()
report.copy(totalFiles = totalFiles.get().takeIf { it >= 0 } ?: report.totalFiles)
}
actual suspend fun openLibraryBook(fileId: String): ByteArray? = withContext(Dispatchers.IO) {