From 817dd062c5d4c05313c38ccdb7a8b3895844ec81 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:00:00 +0100 Subject: [PATCH] Introduce PageStore (used by ImageRepository) --- .../org/fairscan/app/data/ImageRepository.kt | 86 ++++++++----------- .../java/org/fairscan/app/data/PageStore.kt | 53 ++++++++++++ .../fairscan/app/data/ImageRepositoryTest.kt | 9 ++ 3 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/org/fairscan/app/data/PageStore.kt diff --git a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt index bba98e9..d982246 100644 --- a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt +++ b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt @@ -32,6 +32,12 @@ const val SOURCE_DIR_NAME = "sources" const val SCAN_DIR_NAME = "scanned_pages" const val THUMBNAIL_DIR_NAME = "thumbnails" +/** + * Repository responsible for: + * - page persistence (document.json) + * - image files (work, source, thumbnails) + * - page-level operations (add, rotate, move, delete) + */ class ImageRepository( scanRootDir: File, val transformations: ImageTransformations, @@ -52,16 +58,13 @@ class ImageRepository( private val metadataFile = File(scanDir, "document.json") - private val pagesById = mutableMapOf() - private var pages: MutableList = loadPages().also { - pagesById.putAll(it.associateBy { p -> p.id }) - } - private val json = Json { prettyPrint = false encodeDefaults = true } + private var pages: PageStore = PageStore(loadPages()) + private fun loadPages(): MutableList { normalizeLegacyFiles() val filesOnDisk = scanDir.listFiles() @@ -87,9 +90,6 @@ class ImageRepository( } } - private fun indexOfPage(id: String): Int = - pages.indexOfFirst { it.id == id } - private fun loadMetadata(): List { val json = metadataFile.readText() @@ -121,16 +121,16 @@ class ImageRepository( } private fun saveMetadata() { - val metadata = DocumentMetadataV2(pages = pages) + val metadata = DocumentMetadataV2(pages = pages.pages()) metadataFile.writeText(json.encodeToString(metadata)) } fun pages(): List = - pages.map { + pages.pages().map { ScanPage(it.id, it.toMetadata()) } - private fun page(id: String): PageV2? = pagesById[id] + private fun page(id: String): PageV2? = pages.get(id) fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata) { val id = "${System.currentTimeMillis()}" @@ -139,15 +139,15 @@ class ImageRepository( file.writeBytes(pageBytes) writeThumbnail(file) File(sourceDir, fileName).writeBytes(sourceBytes) - val page = PageV2( - id = id, - quad = metadata.normalizedQuad.toSerializable(), - baseRotationDegrees = metadata.baseRotation.degrees, - manualRotationDegrees = metadata.manualRotation.degrees, - isColored = metadata.isColored + pages.addOrReplace( + PageV2( + id = id, + quad = metadata.normalizedQuad.toSerializable(), + baseRotationDegrees = metadata.baseRotation.degrees, + manualRotationDegrees = metadata.manualRotation.degrees, + isColored = metadata.isColored + ) ) - pagesById[page.id] = page - pages.add(page) saveMetadata() } @@ -166,31 +166,27 @@ class ImageRepository( fun PageV2.workFileName() = workFileName(id, manualRotationDegrees) fun rotate(id: String, clockwise: Boolean) { - val index = indexOfPage(id) - if (index < 0) - return - val page = pages[index] + val page = pages.get(id) ?: return val delta = if (clockwise) Rotation.R90 else Rotation.R270 - val currentManualRotation = Rotation.fromDegrees(page.manualRotationDegrees) - val newManualRotation = currentManualRotation.add(delta) - if (newManualRotation == currentManualRotation) { - return // no-op - } + val newRotation = Rotation.fromDegrees(page.manualRotationDegrees).add(delta) val inputFile = File(scanDir, "$id.jpg") - if (!inputFile.exists()) { - return - } - val outputFile = File(scanDir, workFileName(id, newManualRotation.degrees)) - if (!outputFile.exists()) { - val jpegQuality = ExportQuality.BALANCED.jpegQuality - transformations.rotate(inputFile, outputFile, newManualRotation.degrees, jpegQuality) + val outputFile = File(scanDir, workFileName(id, newRotation.degrees)) + + if (inputFile.exists() && !outputFile.exists()) { + transformations.rotate( + inputFile, + outputFile, + newRotation.degrees, + ExportQuality.BALANCED.jpegQuality + ) + } + + pages.update(id) { + it.copy(manualRotationDegrees = newRotation.degrees) } - val updated = page.copy(manualRotationDegrees = newManualRotation.degrees) - pagesById[id] = updated - pages[index] = updated saveMetadata() } @@ -238,22 +234,13 @@ class ImageRepository( } fun movePage(id: String, newIndex: Int) { - val index = indexOfPage(id) - if (index < 0) return - - val page = pages.removeAt(index) - val safeIndex = newIndex.coerceIn(0, pages.size) - pages.add(safeIndex, page) + pages.move(id, newIndex) saveMetadata() } fun delete(id: String) { - val index = indexOfPage(id) - if (index < 0) - return - pages.removeAt(index) + pages.delete(id) saveMetadata() - pagesById.remove(id) getSourceFile(id).delete() scanDir.listFiles() @@ -267,7 +254,6 @@ class ImageRepository( fun clear() { pages.clear() saveMetadata() // "empty" json file - pagesById.clear() thumbnailDir.listFiles()?.forEach { file -> file.delete() diff --git a/app/src/main/java/org/fairscan/app/data/PageStore.kt b/app/src/main/java/org/fairscan/app/data/PageStore.kt new file mode 100644 index 0000000..2cfcfbe --- /dev/null +++ b/app/src/main/java/org/fairscan/app/data/PageStore.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Pierre-Yves Nicolas + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package org.fairscan.app.data + +class PageStore(pages: List) { + + private val pages = LinkedHashMap() + .also { map -> pages.forEach { map.put(it.id, it) } } + + fun pages(): List = + pages.values.toList() + + fun get(id: String): PageV2? = + pages[id] + + fun addOrReplace(page: PageV2) = + pages.put(page.id, page) + + fun update(id: String, transform: (PageV2) -> PageV2) { + val page = pages[id] ?: return + pages[id] = transform(page) + } + + fun delete(id: String) = pages.remove(id) + + fun clear() = pages.clear() + + fun move(id: String, newIndex: Int) { + val page = pages[id] ?: return + + val entries = pages.entries.toList() + .filterNot { it.key == id } + + pages.clear() + + val safeIndex = newIndex.coerceIn(0, entries.size) + entries.take(safeIndex).forEach { pages[it.key] = it.value } + pages.put(id, page) + entries.drop(safeIndex).forEach { pages[it.key] = it.value } + } +} diff --git a/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt b/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt index 35c0330..61da0ee 100644 --- a/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt +++ b/app/src/test/java/org/fairscan/app/data/ImageRepositoryTest.kt @@ -111,6 +111,15 @@ class ImageRepositoryTest { assertThat(repo().imageIds()).containsExactly("1") } + @Test + fun `no json and two files with same id`() { + scanDir().mkdirs() + File(scanDir(), "1768153479486.jpg").writeBytes(byteArrayOf(101, 102, 103)) + File(scanDir(), "1768153479486-270.jpg").writeBytes(byteArrayOf(105, 106, 107)) + val repo = repo() + assertThat(repo.imageIds()).containsExactly("1768153479486") + } + @Test fun `should find existing files at initialization with no json and with rotation`() { scanDir().mkdirs()