Introduce PageStore (used by ImageRepository)
This commit is contained in:
@@ -32,6 +32,12 @@ const val SOURCE_DIR_NAME = "sources"
|
|||||||
const val SCAN_DIR_NAME = "scanned_pages"
|
const val SCAN_DIR_NAME = "scanned_pages"
|
||||||
const val THUMBNAIL_DIR_NAME = "thumbnails"
|
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(
|
class ImageRepository(
|
||||||
scanRootDir: File,
|
scanRootDir: File,
|
||||||
val transformations: ImageTransformations,
|
val transformations: ImageTransformations,
|
||||||
@@ -52,16 +58,13 @@ class ImageRepository(
|
|||||||
|
|
||||||
private val metadataFile = File(scanDir, "document.json")
|
private val metadataFile = File(scanDir, "document.json")
|
||||||
|
|
||||||
private val pagesById = mutableMapOf<String, PageV2>()
|
|
||||||
private var pages: MutableList<PageV2> = loadPages().also {
|
|
||||||
pagesById.putAll(it.associateBy { p -> p.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
prettyPrint = false
|
prettyPrint = false
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var pages: PageStore = PageStore(loadPages())
|
||||||
|
|
||||||
private fun loadPages(): MutableList<PageV2> {
|
private fun loadPages(): MutableList<PageV2> {
|
||||||
normalizeLegacyFiles()
|
normalizeLegacyFiles()
|
||||||
val filesOnDisk = scanDir.listFiles()
|
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<PageV2> {
|
private fun loadMetadata(): List<PageV2> {
|
||||||
val json = metadataFile.readText()
|
val json = metadataFile.readText()
|
||||||
|
|
||||||
@@ -121,16 +121,16 @@ class ImageRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveMetadata() {
|
private fun saveMetadata() {
|
||||||
val metadata = DocumentMetadataV2(pages = pages)
|
val metadata = DocumentMetadataV2(pages = pages.pages())
|
||||||
metadataFile.writeText(json.encodeToString(metadata))
|
metadataFile.writeText(json.encodeToString(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pages(): List<ScanPage> =
|
fun pages(): List<ScanPage> =
|
||||||
pages.map {
|
pages.pages().map {
|
||||||
ScanPage(it.id, it.toMetadata())
|
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) {
|
fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata) {
|
||||||
val id = "${System.currentTimeMillis()}"
|
val id = "${System.currentTimeMillis()}"
|
||||||
@@ -139,15 +139,15 @@ class ImageRepository(
|
|||||||
file.writeBytes(pageBytes)
|
file.writeBytes(pageBytes)
|
||||||
writeThumbnail(file)
|
writeThumbnail(file)
|
||||||
File(sourceDir, fileName).writeBytes(sourceBytes)
|
File(sourceDir, fileName).writeBytes(sourceBytes)
|
||||||
val page = PageV2(
|
pages.addOrReplace(
|
||||||
id = id,
|
PageV2(
|
||||||
quad = metadata.normalizedQuad.toSerializable(),
|
id = id,
|
||||||
baseRotationDegrees = metadata.baseRotation.degrees,
|
quad = metadata.normalizedQuad.toSerializable(),
|
||||||
manualRotationDegrees = metadata.manualRotation.degrees,
|
baseRotationDegrees = metadata.baseRotation.degrees,
|
||||||
isColored = metadata.isColored
|
manualRotationDegrees = metadata.manualRotation.degrees,
|
||||||
|
isColored = metadata.isColored
|
||||||
|
)
|
||||||
)
|
)
|
||||||
pagesById[page.id] = page
|
|
||||||
pages.add(page)
|
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,31 +166,27 @@ class ImageRepository(
|
|||||||
fun PageV2.workFileName() = workFileName(id, manualRotationDegrees)
|
fun PageV2.workFileName() = workFileName(id, manualRotationDegrees)
|
||||||
|
|
||||||
fun rotate(id: String, clockwise: Boolean) {
|
fun rotate(id: String, clockwise: Boolean) {
|
||||||
val index = indexOfPage(id)
|
val page = pages.get(id) ?: return
|
||||||
if (index < 0)
|
|
||||||
return
|
|
||||||
val page = pages[index]
|
|
||||||
|
|
||||||
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
||||||
val currentManualRotation = Rotation.fromDegrees(page.manualRotationDegrees)
|
val newRotation = Rotation.fromDegrees(page.manualRotationDegrees).add(delta)
|
||||||
val newManualRotation = currentManualRotation.add(delta)
|
|
||||||
if (newManualRotation == currentManualRotation) {
|
|
||||||
return // no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputFile = File(scanDir, "$id.jpg")
|
val inputFile = File(scanDir, "$id.jpg")
|
||||||
if (!inputFile.exists()) {
|
val outputFile = File(scanDir, workFileName(id, newRotation.degrees))
|
||||||
return
|
|
||||||
}
|
if (inputFile.exists() && !outputFile.exists()) {
|
||||||
val outputFile = File(scanDir, workFileName(id, newManualRotation.degrees))
|
transformations.rotate(
|
||||||
if (!outputFile.exists()) {
|
inputFile,
|
||||||
val jpegQuality = ExportQuality.BALANCED.jpegQuality
|
outputFile,
|
||||||
transformations.rotate(inputFile, outputFile, newManualRotation.degrees, jpegQuality)
|
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()
|
saveMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,22 +234,13 @@ class ImageRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun movePage(id: String, newIndex: Int) {
|
fun movePage(id: String, newIndex: Int) {
|
||||||
val index = indexOfPage(id)
|
pages.move(id, newIndex)
|
||||||
if (index < 0) return
|
|
||||||
|
|
||||||
val page = pages.removeAt(index)
|
|
||||||
val safeIndex = newIndex.coerceIn(0, pages.size)
|
|
||||||
pages.add(safeIndex, page)
|
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(id: String) {
|
fun delete(id: String) {
|
||||||
val index = indexOfPage(id)
|
pages.delete(id)
|
||||||
if (index < 0)
|
|
||||||
return
|
|
||||||
pages.removeAt(index)
|
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
pagesById.remove(id)
|
|
||||||
|
|
||||||
getSourceFile(id).delete()
|
getSourceFile(id).delete()
|
||||||
scanDir.listFiles()
|
scanDir.listFiles()
|
||||||
@@ -267,7 +254,6 @@ class ImageRepository(
|
|||||||
fun clear() {
|
fun clear() {
|
||||||
pages.clear()
|
pages.clear()
|
||||||
saveMetadata() // "empty" json file
|
saveMetadata() // "empty" json file
|
||||||
pagesById.clear()
|
|
||||||
|
|
||||||
thumbnailDir.listFiles()?.forEach {
|
thumbnailDir.listFiles()?.forEach {
|
||||||
file -> file.delete()
|
file -> file.delete()
|
||||||
|
|||||||
53
app/src/main/java/org/fairscan/app/data/PageStore.kt
Normal file
53
app/src/main/java/org/fairscan/app/data/PageStore.kt
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.fairscan.app.data
|
||||||
|
|
||||||
|
class PageStore(pages: List<PageV2>) {
|
||||||
|
|
||||||
|
private val pages = LinkedHashMap<String, PageV2>()
|
||||||
|
.also { map -> pages.forEach { map.put(it.id, it) } }
|
||||||
|
|
||||||
|
fun pages(): List<PageV2> =
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,6 +111,15 @@ class ImageRepositoryTest {
|
|||||||
assertThat(repo().imageIds()).containsExactly("1")
|
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
|
@Test
|
||||||
fun `should find existing files at initialization with no json and with rotation`() {
|
fun `should find existing files at initialization with no json and with rotation`() {
|
||||||
scanDir().mkdirs()
|
scanDir().mkdirs()
|
||||||
|
|||||||
Reference in New Issue
Block a user