Make ImageRepository thread-safe (#147)
This commit is contained in:
committed by
GitHub
parent
516dd75e9c
commit
92914c1730
@@ -142,6 +142,7 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.assertj)
|
testImplementation(libs.assertj)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -33,27 +32,29 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.fairscan.app.data.ImageRepository
|
import org.fairscan.app.data.ImageRepository
|
||||||
import org.fairscan.app.domain.CapturedPage
|
import org.fairscan.app.domain.CapturedPage
|
||||||
import org.fairscan.app.domain.PageViewKey
|
import org.fairscan.app.domain.PageViewKey
|
||||||
|
import org.fairscan.app.domain.ScanPage
|
||||||
import org.fairscan.app.ui.NavigationState
|
import org.fairscan.app.ui.NavigationState
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
||||||
|
|
||||||
// TODO ImageRepository should be made thread-safe
|
|
||||||
private val repositoryDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
|
||||||
|
|
||||||
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
|
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
|
||||||
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
|
||||||
|
|
||||||
private val _pages = MutableStateFlow(imageRepository.pages())
|
private val _pages = MutableStateFlow<List<ScanPage>>(emptyList())
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_pages.value = imageRepository.pages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val documentUiModel: StateFlow<DocumentUiModel> =
|
val documentUiModel: StateFlow<DocumentUiModel> =
|
||||||
_pages.map { pages ->
|
_pages.map { pages ->
|
||||||
DocumentUiModel(
|
DocumentUiModel(
|
||||||
pageKeys = pages.map { p ->
|
pageKeys = pages.map { it.key() }.toImmutableList(),
|
||||||
PageViewKey(p.id, p.manualRotation)
|
|
||||||
}.toImmutableList(),
|
|
||||||
imageLoader = ::getBitmap,
|
imageLoader = ::getBitmap,
|
||||||
thumbnailLoader = ::getThumbnail,
|
thumbnailLoader = ::getThumbnail,
|
||||||
)
|
)
|
||||||
@@ -73,7 +74,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
fun rotateImage(id: String, clockwise: Boolean) {
|
fun rotateImage(id: String, clockwise: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val pages = withContext(repositoryDispatcher) {
|
val pages = withContext(Dispatchers.IO) {
|
||||||
imageRepository.rotate(id, clockwise)
|
imageRepository.rotate(id, clockwise)
|
||||||
imageRepository.pages()
|
imageRepository.pages()
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
fun movePage(id: String, newIndex: Int) {
|
fun movePage(id: String, newIndex: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val pages = withContext(repositoryDispatcher) {
|
val pages = withContext(Dispatchers.IO) {
|
||||||
imageRepository.movePage(id, newIndex)
|
imageRepository.movePage(id, newIndex)
|
||||||
imageRepository.pages()
|
imageRepository.pages()
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
fun deletePage(id: String) {
|
fun deletePage(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val pages = withContext(repositoryDispatcher) {
|
val pages = withContext(Dispatchers.IO) {
|
||||||
imageRepository.delete(id)
|
imageRepository.delete(id)
|
||||||
imageRepository.pages()
|
imageRepository.pages()
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
fun startNewDocument() {
|
fun startNewDocument() {
|
||||||
_pages.value = persistentListOf()
|
_pages.value = persistentListOf()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(repositoryDispatcher) {
|
withContext(Dispatchers.IO) {
|
||||||
imageRepository.clear()
|
imageRepository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,10 +123,8 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
fun handleImageCaptured(capturedPage: CapturedPage) {
|
fun handleImageCaptured(capturedPage: CapturedPage) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val sourceJpeg = withContext(Dispatchers.IO) {
|
val pages = withContext(Dispatchers.IO) {
|
||||||
capturedPage.sourceJpeg.await()
|
val sourceJpeg = capturedPage.sourceJpeg.await()
|
||||||
}
|
|
||||||
val pages = withContext(repositoryDispatcher) {
|
|
||||||
imageRepository.add(
|
imageRepository.add(
|
||||||
capturedPage.pageJpeg,
|
capturedPage.pageJpeg,
|
||||||
sourceJpeg,
|
sourceJpeg,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app.data
|
package org.fairscan.app.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import kotlinx.serialization.json.int
|
import kotlinx.serialization.json.int
|
||||||
@@ -56,6 +58,8 @@ class ImageRepository(
|
|||||||
if (!exists()) mkdirs()
|
if (!exists()) mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val metadataFile = File(scanDir, "document.json")
|
private val metadataFile = File(scanDir, "document.json")
|
||||||
|
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
@@ -121,17 +125,17 @@ class ImageRepository(
|
|||||||
metadataFile.writeText(json.encodeToString(metadata))
|
metadataFile.writeText(json.encodeToString(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pages(): List<ScanPage> =
|
suspend fun pages(): List<ScanPage> = mutex.withLock {
|
||||||
pages.pages().mapNotNull {
|
pages.pages().mapNotNull {
|
||||||
runCatching {
|
runCatching {
|
||||||
val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees)
|
val manualRotation = Rotation.fromDegrees(it.manualRotationDegrees)
|
||||||
ScanPage(it.id, manualRotation, it.toMetadata())
|
ScanPage(it.id, manualRotation, it.toMetadata())
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun page(id: String): PageV2? = pages.get(id)
|
suspend fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata)
|
||||||
|
= mutex.withLock {
|
||||||
fun add(pageBytes: ByteArray, sourceBytes: ByteArray, metadata: PageMetadata) {
|
|
||||||
val id = "${System.currentTimeMillis()}"
|
val id = "${System.currentTimeMillis()}"
|
||||||
val fileName = "$id.jpg"
|
val fileName = "$id.jpg"
|
||||||
val file = File(scanDir, fileName)
|
val file = File(scanDir, fileName)
|
||||||
@@ -164,8 +168,8 @@ class ImageRepository(
|
|||||||
|
|
||||||
fun PageV2.workFileName() = workFileName(id, manualRotationDegrees)
|
fun PageV2.workFileName() = workFileName(id, manualRotationDegrees)
|
||||||
|
|
||||||
fun rotate(id: String, clockwise: Boolean) {
|
suspend fun rotate(id: String, clockwise: Boolean) = mutex.withLock {
|
||||||
val page = pages.get(id) ?: return
|
val page = pages.get(id) ?: return@withLock
|
||||||
|
|
||||||
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
val delta = if (clockwise) Rotation.R90 else Rotation.R270
|
||||||
val newRotation = Rotation.fromDegrees(page.manualRotationDegrees).add(delta)
|
val newRotation = Rotation.fromDegrees(page.manualRotationDegrees).add(delta)
|
||||||
@@ -194,13 +198,6 @@ class ImageRepository(
|
|||||||
return (if (file.exists()) file else null)?.readBytes()
|
return (if (file.exists()) file else null)?.readBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun jpegBytes(id: String): ByteArray? {
|
|
||||||
val page = page(id)
|
|
||||||
if (page == null) return null
|
|
||||||
val file = File(scanDir, page.workFileName())
|
|
||||||
return (if (file.exists()) file else null)?.readBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sourceJpegBytes(id: String): ByteArray? {
|
fun sourceJpegBytes(id: String): ByteArray? {
|
||||||
val file = getSourceFile(id)
|
val file = getSourceFile(id)
|
||||||
return if (file.exists()) file.readBytes() else null
|
return if (file.exists()) file.readBytes() else null
|
||||||
@@ -211,10 +208,7 @@ class ImageRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnail(key: PageViewKey): ByteArray? {
|
fun getThumbnail(key: PageViewKey): ByteArray? {
|
||||||
val thumbFile = getThumbnailFile(key)
|
val thumbFile = File(thumbnailDir, workFileName(key))
|
||||||
if (thumbFile == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!thumbFile.exists()) {
|
if (!thumbFile.exists()) {
|
||||||
val workFile = File(scanDir, workFileName(key))
|
val workFile = File(scanDir, workFileName(key))
|
||||||
if (!workFile.exists()) return null
|
if (!workFile.exists()) return null
|
||||||
@@ -228,16 +222,12 @@ class ImageRepository(
|
|||||||
transformations.resize(originalFile, thumbFile, thumbnailSizePx)
|
transformations.resize(originalFile, thumbFile, thumbnailSizePx)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getThumbnailFile(key: PageViewKey): File? {
|
suspend fun movePage(id: String, newIndex: Int) = mutex.withLock {
|
||||||
return File(thumbnailDir, workFileName(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun movePage(id: String, newIndex: Int) {
|
|
||||||
pages.move(id, newIndex)
|
pages.move(id, newIndex)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(id: String) {
|
suspend fun delete(id: String) = mutex.withLock {
|
||||||
pages.delete(id)
|
pages.delete(id)
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
|
|
||||||
@@ -250,7 +240,7 @@ class ImageRepository(
|
|||||||
?.forEach { it.delete() }
|
?.forEach { it.delete() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
suspend fun clear() = mutex.withLock {
|
||||||
pages.clear()
|
pages.clear()
|
||||||
saveMetadata() // "empty" json file
|
saveMetadata() // "empty" json file
|
||||||
|
|
||||||
|
|||||||
@@ -20,20 +20,19 @@ import org.fairscan.imageprocessing.resizeForMaxPixels
|
|||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
import org.opencv.core.Mat
|
import org.opencv.core.Mat
|
||||||
import org.opencv.core.MatOfByte
|
import org.opencv.core.MatOfByte
|
||||||
import org.opencv.core.MatOfInt
|
|
||||||
import org.opencv.imgcodecs.Imgcodecs
|
import org.opencv.imgcodecs.Imgcodecs
|
||||||
|
|
||||||
fun jpegsForExport(
|
suspend fun jpegsForExport(
|
||||||
imageRepository: ImageRepository,
|
imageRepository: ImageRepository,
|
||||||
exportQuality: ExportQuality
|
exportQuality: ExportQuality
|
||||||
): Sequence<ByteArray> {
|
): Sequence<ByteArray> {
|
||||||
|
|
||||||
val pages = imageRepository.pages().asSequence()
|
val pages = imageRepository.pages().asSequence()
|
||||||
return when (exportQuality) {
|
return when (exportQuality) {
|
||||||
ExportQuality.BALANCED -> pages.mapNotNull { imageRepository.jpegBytes(it.id) }
|
ExportQuality.BALANCED -> pages.mapNotNull { imageRepository.jpegBytes(it.key()) }
|
||||||
|
|
||||||
ExportQuality.LOW -> pages.mapNotNull { page ->
|
ExportQuality.LOW -> pages.mapNotNull { page ->
|
||||||
imageRepository.jpegBytes(page.id)?.let { jpeg ->
|
imageRepository.jpegBytes(page.key())?.let { jpeg ->
|
||||||
resizeJpegBytesForMaxPixels(
|
resizeJpegBytesForMaxPixels(
|
||||||
jpegBytes = jpeg,
|
jpegBytes = jpeg,
|
||||||
maxPixels = exportQuality.maxPixels.toDouble(),
|
maxPixels = exportQuality.maxPixels.toDouble(),
|
||||||
@@ -49,7 +48,7 @@ fun jpegsForExport(
|
|||||||
if (sourceJpegBytes != null && pageMetadata != null)
|
if (sourceJpegBytes != null && pageMetadata != null)
|
||||||
prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality)
|
prepareJpegForHigh(sourceJpegBytes, pageMetadata, manualRotation, exportQuality)
|
||||||
else
|
else
|
||||||
imageRepository.jpegBytes(page.id)
|
imageRepository.jpegBytes(page.key())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ data class ScanPage(
|
|||||||
val id: String,
|
val id: String,
|
||||||
val manualRotation: Rotation,
|
val manualRotation: Rotation,
|
||||||
val metadata: PageMetadata?,
|
val metadata: PageMetadata?,
|
||||||
)
|
) {
|
||||||
|
fun key(): PageViewKey = PageViewKey(id, manualRotation)
|
||||||
|
}
|
||||||
|
|
||||||
data class PageViewKey(
|
data class PageViewKey(
|
||||||
val pageId: String,
|
val pageId: String,
|
||||||
|
|||||||
@@ -119,10 +119,8 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun currentPageKeys(): ImmutableList<PageViewKey> =
|
private suspend fun currentPageKeys(): ImmutableList<PageViewKey> =
|
||||||
imageRepository.pages().map {
|
imageRepository.pages().map { it.key() }.toImmutableList()
|
||||||
PageViewKey(it.id, it.manualRotation)
|
|
||||||
}.toImmutableList()
|
|
||||||
|
|
||||||
fun prepareExportIfNeeded() {
|
fun prepareExportIfNeeded() {
|
||||||
ensureValidFilename()
|
ensureValidFilename()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package org.fairscan.app.data
|
|||||||
|
|
||||||
import kotlinx.collections.immutable.PersistentList
|
import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.fairscan.app.domain.PageMetadata
|
import org.fairscan.app.domain.PageMetadata
|
||||||
import org.fairscan.app.domain.PageViewKey
|
import org.fairscan.app.domain.PageViewKey
|
||||||
@@ -60,7 +61,7 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun add_image() {
|
fun add_image() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
val bytes = byteArrayOf(101, 102, 103)
|
val bytes = byteArrayOf(101, 102, 103)
|
||||||
@@ -82,7 +83,7 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun delete_image() {
|
fun delete_image() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
val bytes = byteArrayOf(101, 102, 103)
|
val bytes = byteArrayOf(101, 102, 103)
|
||||||
repo.add(bytes, byteArrayOf(51), metadata1)
|
repo.add(bytes, byteArrayOf(51), metadata1)
|
||||||
@@ -98,28 +99,28 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun delete_unknown_id() {
|
fun delete_unknown_id() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
repo.delete("x")
|
repo.delete("x")
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should find existing files at initialization with no json`() {
|
fun `should find existing files at initialization with no json`() = runTest {
|
||||||
scanDir().mkdirs()
|
scanDir().mkdirs()
|
||||||
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
||||||
assertThat(repo().imageIds()).containsExactly("1")
|
assertThat(repo().imageIds()).containsExactly("1")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should find existing files at initialization if json is invalid`() {
|
fun `should find existing files at initialization if json is invalid`() = runTest {
|
||||||
writeDocumentDotJson("xxx")
|
writeDocumentDotJson("xxx")
|
||||||
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
File(scanDir(), "1.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
||||||
assertThat(repo().imageIds()).containsExactly("1")
|
assertThat(repo().imageIds()).containsExactly("1")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `no json and two files with same id`() {
|
fun `no json and two files with same id`() = runTest {
|
||||||
scanDir().mkdirs()
|
scanDir().mkdirs()
|
||||||
File(scanDir(), "1768153479486.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
File(scanDir(), "1768153479486.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
||||||
File(scanDir(), "1768153479486-270.jpg").writeBytes(byteArrayOf(105, 106, 107))
|
File(scanDir(), "1768153479486-270.jpg").writeBytes(byteArrayOf(105, 106, 107))
|
||||||
@@ -128,24 +129,24 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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`() = runTest {
|
||||||
scanDir().mkdirs()
|
scanDir().mkdirs()
|
||||||
val bytes = byteArrayOf(101, 102, 103)
|
val bytes = byteArrayOf(101, 102, 103)
|
||||||
File(scanDir(), "1-90.jpg").writeBytes(bytes)
|
File(scanDir(), "1-90.jpg").writeBytes(bytes)
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).containsExactly("1")
|
assertThat(repo.imageIds()).containsExactly("1")
|
||||||
assertThat(repo.jpegBytes("1")).isEqualTo(bytes)
|
assertThat(repo.jpegBytes(PageViewKey("1", R0))).isEqualTo(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should filter pages in json at initialization`() {
|
fun `should filter pages in json at initialization`() = runTest {
|
||||||
writeDocumentDotJson("""{"pages":[{"file":"1.jpg"}, {"file":"2.jpg"}]}""")
|
writeDocumentDotJson("""{"pages":[{"file":"1.jpg"}, {"file":"2.jpg"}]}""")
|
||||||
File(scanDir(), "2.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
File(scanDir(), "2.jpg").writeBytes(byteArrayOf(101, 102, 103))
|
||||||
assertThat(repo().imageIds()).containsExactly("2")
|
assertThat(repo().imageIds()).containsExactly("2")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should rename rotated files with no base file`() {
|
fun `should rename rotated files with no base file`() = runTest {
|
||||||
scanDir().mkdirs()
|
scanDir().mkdirs()
|
||||||
val bytes = byteArrayOf(105, 106, 107)
|
val bytes = byteArrayOf(105, 106, 107)
|
||||||
File(scanDir(), "123-90.jpg").writeBytes(bytes)
|
File(scanDir(), "123-90.jpg").writeBytes(bytes)
|
||||||
@@ -157,24 +158,24 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should rename rotated files with no base file but listed in json`() {
|
fun `should rename rotated files with no base file but listed in json`() = runTest {
|
||||||
writeDocumentDotJson("""{"pages":[{"file":"1-90.jpg"}]}""")
|
writeDocumentDotJson("""{"pages":[{"file":"1-90.jpg"}]}""")
|
||||||
val bytes = byteArrayOf(105, 106, 107)
|
val bytes = byteArrayOf(105, 106, 107)
|
||||||
File(scanDir(), "1-90.jpg").writeBytes(bytes)
|
File(scanDir(), "1-90.jpg").writeBytes(bytes)
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).containsExactly("1")
|
assertThat(repo.imageIds()).containsExactly("1")
|
||||||
assertThat(repo.jpegBytes("1")).isEqualTo(bytes)
|
assertThat(repo.jpegBytes(PageViewKey("1", R0))).isEqualTo(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should return null on invalid id`() {
|
fun `should return null on invalid id`() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
assertThat(repo.jpegBytes("x")).isNull()
|
assertThat(repo.jpegBytes(PageViewKey("x", R0))).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clear should delete pages`() {
|
fun `clear should delete pages`() = runTest {
|
||||||
val bytes = byteArrayOf(101, 102, 103)
|
val bytes = byteArrayOf(101, 102, 103)
|
||||||
val repo1 = repo()
|
val repo1 = repo()
|
||||||
repo1.add(bytes, byteArrayOf(51), metadata1)
|
repo1.add(bytes, byteArrayOf(51), metadata1)
|
||||||
@@ -189,7 +190,7 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun rotate() {
|
fun rotate() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
repo.add(byteArrayOf(101, 102, 103), byteArrayOf(51), metadata1)
|
repo.add(byteArrayOf(101, 102, 103), byteArrayOf(51), metadata1)
|
||||||
assertThat(repo.pages().last().metadata).isEqualTo(metadata1)
|
assertThat(repo.pages().last().metadata).isEqualTo(metadata1)
|
||||||
@@ -207,14 +208,14 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun rotate_unknown_id() {
|
fun rotate_unknown_id() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
repo.rotate("x", true)
|
repo.rotate("x", true)
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun movePage() {
|
fun movePage() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
repo.add(byteArrayOf(101), byteArrayOf(51), metadata1)
|
repo.add(byteArrayOf(101), byteArrayOf(51), metadata1)
|
||||||
Thread.sleep(1L) // to avoid file name clashes
|
Thread.sleep(1L) // to avoid file name clashes
|
||||||
@@ -229,7 +230,7 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun move_unknown_id() {
|
fun move_unknown_id() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
repo.movePage("x", 0)
|
repo.movePage("x", 0)
|
||||||
assertThat(repo.imageIds()).isEmpty()
|
assertThat(repo.imageIds()).isEmpty()
|
||||||
@@ -250,7 +251,7 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `pages with invalid metadata should be skipped`() {
|
fun `pages with invalid metadata should be skipped`() = runTest {
|
||||||
val bytes = byteArrayOf(105, 106, 107)
|
val bytes = byteArrayOf(105, 106, 107)
|
||||||
|
|
||||||
writeDocumentDotJson("""{"version":2, "pages":[{"id":"1", "manualRotationDegrees":90}]}""")
|
writeDocumentDotJson("""{"version":2, "pages":[{"id":"1", "manualRotationDegrees":90}]}""")
|
||||||
@@ -265,7 +266,7 @@ class ImageRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun last_added_source_file() {
|
fun last_added_source_file() = runTest {
|
||||||
val repo = repo()
|
val repo = repo()
|
||||||
assertThat(repo.lastAddedSourceFile()).isNull()
|
assertThat(repo.lastAddedSourceFile()).isNull()
|
||||||
repo.add(byteArrayOf(101), byteArrayOf(51), metadata1)
|
repo.add(byteArrayOf(101), byteArrayOf(51), metadata1)
|
||||||
@@ -298,6 +299,6 @@ class ImageRepositoryTest {
|
|||||||
File(scanDir(), "document.json").writeText(json)
|
File(scanDir(), "document.json").writeText(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageRepository.imageIds(): PersistentList<String> =
|
suspend fun ImageRepository.imageIds(): PersistentList<String> =
|
||||||
pages().map { it.id }.toPersistentList()
|
pages().map { it.id }.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ protobufJavaLite = "4.34.1"
|
|||||||
kotlinSerialization = "1.10.0"
|
kotlinSerialization = "1.10.0"
|
||||||
reorderable = "3.0.0"
|
reorderable = "3.0.0"
|
||||||
jetbrainsKotlinJvm = "2.3.10"
|
jetbrainsKotlinJvm = "2.3.10"
|
||||||
|
coroutines-test = "1.10.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -62,6 +63,8 @@ zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zooma
|
|||||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||||
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
|
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
|
||||||
|
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines-test" }
|
||||||
|
|
||||||
|
|
||||||
assertj = { group="org.assertj", name="assertj-core", version.ref = "assertj" }
|
assertj = { group="org.assertj", name="assertj-core", version.ref = "assertj" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user