merge(refactor): layers-refactor into develop

This commit is contained in:
Acclorite 2025-06-05 22:53:44 +03:00
commit dd7998c600
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
567 changed files with 4483 additions and 4245 deletions

View file

@ -37,7 +37,7 @@
<!-- Main activity --> <!-- Main activity -->
<activity <activity
android:name=".ui.main.MainActivity" android:name=".presentation.main.MainActivity"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
android:exported="true" android:exported="true"
android:theme="@style/Splash" android:theme="@style/Splash"
@ -50,7 +50,7 @@
<!-- Crash activity --> <!-- Crash activity -->
<activity <activity
android:name=".ui.crash.CrashActivity" android:name=".presentation.crash.CrashActivity"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
android:exported="false" android:exported="false"
android:theme="@style/BookStory" android:theme="@style/BookStory"

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.util package ua.acclorite.book_story.core
import android.graphics.Bitmap import android.graphics.Bitmap

View file

@ -9,12 +9,10 @@ package ua.acclorite.book_story.core.crash
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Process import android.os.Process
import android.util.Log import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.ui.crash.CrashActivity import ua.acclorite.book_story.presentation.crash.CrashActivity
import kotlin.system.exitProcess import kotlin.system.exitProcess
private const val CRASH_TAG = "CRASH, APP"
class CrashHandler( class CrashHandler(
private val context: Context private val context: Context
) : Thread.UncaughtExceptionHandler { ) : Thread.UncaughtExceptionHandler {
@ -22,7 +20,7 @@ class CrashHandler(
override fun uncaughtException(thread: Thread, throwable: Throwable) { override fun uncaughtException(thread: Thread, throwable: Throwable) {
val crashLog = throwable.stackTraceToString() val crashLog = throwable.stackTraceToString()
Log.e(CRASH_TAG, crashLog) logE(crashLog)
if (!CrashUtils.saveCrashLog(context, crashLog)) return if (!CrashUtils.saveCrashLog(context, crashLog)) return
val intent = Intent(context, CrashActivity::class.java).apply { val intent = Intent(context, CrashActivity::class.java).apply {

View file

@ -0,0 +1,33 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.core.data
import kotlinx.collections.immutable.persistentListOf
object CoreData {
val languages = persistentListOf(
Pair("en", "English"),
Pair("uk", "Українська"),
Pair("de", "Deutsch"),
Pair("ar", "اَلْعَرَبِيَّةُ"),
Pair("es", "Español"),
Pair("tr", "Türkçe"),
Pair("fr", "Français"),
Pair("pl", "Polski"),
Pair("it", "Italiano"),
Pair("zh", "汉语"),
Pair("hi", "हिन्दी"),
// Currently as "pt" (which is Portuguese) instead of "pt-BR"
Pair("pt", "Português (Brasil)"),
Pair("ta", "தமிழ்"),
Pair("ml", "മലയാളം"),
Pair("cs", "Čeština"),
Pair("ro", "Română"),
Pair("be", "Беларуская"),
Pair("ja", "日本語")
)
}

View file

@ -0,0 +1,28 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.core.data
import kotlinx.collections.immutable.persistentListOf
object ExtensionsData {
val fileExtensions = persistentListOf(
".epub",
".pdf",
".fb2",
".txt",
".html",
".htm",
".md"
)
val imageExtensions = persistentListOf(
".png",
".jpg",
".jpeg",
".gif"
)
}

View file

@ -0,0 +1,15 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.core.data
object LinkData {
const val RELEASES = "https://www.github.com/Acclorite/book-story/releases/latest"
const val ISSUES = "https://www.github.com/Acclorite/book-story/issues"
const val CONTRIBUTORS = "https://github.com/Acclorite/book-story/graphs/contributors"
const val TRANSLATION = "https://hosted.weblate.org/projects/book-story"
const val SUPPORT = "https://patreon.com/Acclorite"
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.presentation.core.util package ua.acclorite.book_story.core.helpers
fun <T> MutableList<T>.addAll(calculation: () -> List<T>) { fun <T> MutableList<T>.addAll(calculation: () -> List<T>) {
addAll(calculation()) addAll(calculation())

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.presentation.core.util package ua.acclorite.book_story.core.helpers
fun String.removeTrailingZero(): String { fun String.removeTrailingZero(): String {
if (!this.contains('.')) if (!this.contains('.'))
@ -66,14 +66,3 @@ fun Char.isVisibleCharacter(): Boolean {
else -> false else -> false
} }
} }
fun <T> compareByWithOrder(
descending: Boolean,
selector: (T) -> Comparable<*>?
): Comparator<T> {
return if (descending) {
compareByDescending(selector)
} else {
compareBy(selector)
}
}

View file

@ -0,0 +1,18 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.core.helpers
fun <T> compareByWithOrder(
descending: Boolean,
selector: (T) -> Comparable<*>?
): Comparator<T> {
return if (descending) {
compareByDescending(selector)
} else {
compareBy(selector)
}
}

View file

@ -0,0 +1,25 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.core.log
import android.util.Log
fun logI(message: String) {
Log.i("INFO", message)
}
fun logD(message: String) {
Log.d("DEBUG", message)
}
fun logW(message: String) {
Log.w("WARNING", message)
}
fun logE(message: String) {
Log.e("ERROR", message)
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.ui package ua.acclorite.book_story.core.ui
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable

View file

@ -20,6 +20,8 @@ import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper
import ua.acclorite.book_story.data.mapper.category_sort.CategorySortSortMapperImpl import ua.acclorite.book_story.data.mapper.category_sort.CategorySortSortMapperImpl
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl
import ua.acclorite.book_story.data.mapper.file.FileMapper
import ua.acclorite.book_story.data.mapper.file.FileMapperImpl
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
@ -123,6 +125,12 @@ abstract class RepositoryModule {
categorySortSortMapperImpl: CategorySortSortMapperImpl categorySortSortMapperImpl: CategorySortSortMapperImpl
): CategorySortMapper ): CategorySortMapper
@Binds
@Singleton
abstract fun bindFileMapper(
fileMapperImpl: FileMapperImpl
): FileMapper
@Binds @Binds
@Singleton @Singleton
abstract fun bindFileParser( abstract fun bindFileParser(

View file

@ -0,0 +1,34 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ua.acclorite.book_story.data.service.CoverImageHandlerImpl
import ua.acclorite.book_story.data.service.FileProviderImpl
import ua.acclorite.book_story.domain.service.CoverImageHandler
import ua.acclorite.book_story.domain.service.FileProvider
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
@Singleton
abstract fun bindFileProvider(
fileProviderImpl: FileProviderImpl
): FileProvider
@Binds
@Singleton
abstract fun bindCoverImageHandler(
coverImageHandlerImpl: CoverImageHandlerImpl
): CoverImageHandler
}

View file

@ -8,7 +8,7 @@ package ua.acclorite.book_story.data.local.dto
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import ua.acclorite.book_story.domain.library.display.LibrarySortOrder import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Entity @Entity
data class CategorySortEntity( data class CategorySortEntity(

View file

@ -40,16 +40,13 @@ interface BookDao {
suspend fun searchBooks(query: String): List<BookEntity> suspend fun searchBooks(query: String): List<BookEntity>
@Query("SELECT * FROM bookentity WHERE id=:id") @Query("SELECT * FROM bookentity WHERE id=:id")
suspend fun findBookById(id: Int): BookEntity suspend fun findBookById(id: Int): BookEntity?
@Query("SELECT * FROM bookentity WHERE id IN (:ids)")
suspend fun findBooksById(ids: List<Int>): List<BookEntity>
@Delete @Delete
suspend fun deleteBooks(books: List<BookEntity>) suspend fun deleteBook(book: BookEntity): Int
@Update @Update
suspend fun updateBooks(books: List<BookEntity>) suspend fun updateBook(book: BookEntity): Int
/* - - - - - - - - - - - - - - - - - - - - - - */ /* - - - - - - - - - - - - - - - - - - - - - - */
@ -58,21 +55,21 @@ interface BookDao {
suspend fun getHistory(): List<HistoryEntity> suspend fun getHistory(): List<HistoryEntity>
@Query("SELECT * FROM historyentity WHERE bookId = :bookId ORDER BY time DESC LIMIT 1") @Query("SELECT * FROM historyentity WHERE bookId = :bookId ORDER BY time DESC LIMIT 1")
fun getLatestHistoryForBook(bookId: Int): HistoryEntity? fun getHistoryForBook(bookId: Int): HistoryEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHistory( suspend fun insertHistory(
history: List<HistoryEntity> history: HistoryEntity
) )
@Query("DELETE FROM historyentity") @Query("DELETE FROM historyentity")
suspend fun deleteWholeHistory() suspend fun deleteWholeHistory(): Int
@Query("DELETE FROM historyentity WHERE bookId = :bookId") @Query("DELETE FROM historyentity WHERE bookId = :bookId")
suspend fun deleteBookHistory(bookId: Int) suspend fun deleteHistoryForBook(bookId: Int): Int
@Delete @Delete
suspend fun deleteHistory(history: List<HistoryEntity>) suspend fun deleteHistory(history: HistoryEntity): Int
/* - - - - - - - - - - - - - - - - - - - - - - */ /* - - - - - - - - - - - - - - - - - - - - - - */

View file

@ -7,10 +7,9 @@
package ua.acclorite.book_story.data.mapper.book package ua.acclorite.book_story.data.mapper.book
import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.model.library.Book
interface BookMapper { interface BookMapper {
suspend fun toBookEntity(book: Book): BookEntity fun toBookEntity(book: Book): BookEntity
fun toBook(bookEntity: BookEntity): Book
suspend fun toBook(bookEntity: BookEntity): Book
} }

View file

@ -8,13 +8,13 @@ package ua.acclorite.book_story.data.mapper.book
import androidx.core.net.toUri import androidx.core.net.toUri
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.ui.UIText
import javax.inject.Inject import javax.inject.Inject
class BookMapperImpl @Inject constructor() : BookMapper { class BookMapperImpl @Inject constructor() : BookMapper {
override suspend fun toBookEntity(book: Book): BookEntity { override fun toBookEntity(book: Book): BookEntity {
return BookEntity( return BookEntity(
id = book.id, id = book.id,
title = book.title, title = book.title,
@ -29,7 +29,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
) )
} }
override suspend fun toBook(bookEntity: BookEntity): Book { override fun toBook(bookEntity: BookEntity): Book {
return Book( return Book(
id = bookEntity.id, id = bookEntity.id,
title = bookEntity.title, title = bookEntity.title,

View file

@ -7,10 +7,9 @@
package ua.acclorite.book_story.data.mapper.category package ua.acclorite.book_story.data.mapper.category
import ua.acclorite.book_story.data.local.dto.CategoryEntity import ua.acclorite.book_story.data.local.dto.CategoryEntity
import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.model.library.Category
interface CategoryMapper { interface CategoryMapper {
suspend fun toCategoryEntity(category: Category): CategoryEntity fun toCategoryEntity(category: Category): CategoryEntity
fun toCategory(categoryEntity: CategoryEntity): Category
suspend fun toCategory(categoryEntity: CategoryEntity): Category
} }

View file

@ -7,11 +7,11 @@
package ua.acclorite.book_story.data.mapper.category package ua.acclorite.book_story.data.mapper.category
import ua.acclorite.book_story.data.local.dto.CategoryEntity import ua.acclorite.book_story.data.local.dto.CategoryEntity
import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.model.library.Category
import javax.inject.Inject import javax.inject.Inject
class CategoryMapperImpl @Inject constructor() : CategoryMapper { class CategoryMapperImpl @Inject constructor() : CategoryMapper {
override suspend fun toCategoryEntity(category: Category): CategoryEntity { override fun toCategoryEntity(category: Category): CategoryEntity {
return CategoryEntity( return CategoryEntity(
id = category.id, id = category.id,
title = category.title, title = category.title,
@ -19,7 +19,7 @@ class CategoryMapperImpl @Inject constructor() : CategoryMapper {
) )
} }
override suspend fun toCategory(categoryEntity: CategoryEntity): Category { override fun toCategory(categoryEntity: CategoryEntity): Category {
return Category( return Category(
id = categoryEntity.id, id = categoryEntity.id,
title = categoryEntity.title, title = categoryEntity.title,

View file

@ -7,10 +7,9 @@
package ua.acclorite.book_story.data.mapper.category_sort package ua.acclorite.book_story.data.mapper.category_sort
import ua.acclorite.book_story.data.local.dto.CategorySortEntity import ua.acclorite.book_story.data.local.dto.CategorySortEntity
import ua.acclorite.book_story.domain.library.category.CategorySort import ua.acclorite.book_story.domain.model.library.CategorySort
interface CategorySortMapper { interface CategorySortMapper {
suspend fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity
fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort
suspend fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort
} }

View file

@ -7,11 +7,11 @@
package ua.acclorite.book_story.data.mapper.category_sort package ua.acclorite.book_story.data.mapper.category_sort
import ua.acclorite.book_story.data.local.dto.CategorySortEntity import ua.acclorite.book_story.data.local.dto.CategorySortEntity
import ua.acclorite.book_story.domain.library.category.CategorySort import ua.acclorite.book_story.domain.model.library.CategorySort
import javax.inject.Inject import javax.inject.Inject
class CategorySortSortMapperImpl @Inject constructor() : CategorySortMapper { class CategorySortSortMapperImpl @Inject constructor() : CategorySortMapper {
override suspend fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity { override fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity {
return CategorySortEntity( return CategorySortEntity(
categoryId = categorySort.categoryId, categoryId = categorySort.categoryId,
sortOrder = categorySort.sortOrder, sortOrder = categorySort.sortOrder,
@ -19,7 +19,7 @@ class CategorySortSortMapperImpl @Inject constructor() : CategorySortMapper {
) )
} }
override suspend fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort { override fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort {
return CategorySort( return CategorySort(
categoryId = categorySortEntity.categoryId, categoryId = categorySortEntity.categoryId,
sortOrder = categorySortEntity.sortOrder, sortOrder = categorySortEntity.sortOrder,

View file

@ -7,10 +7,9 @@
package ua.acclorite.book_story.data.mapper.color_preset package ua.acclorite.book_story.data.mapper.color_preset
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.model.reader.ColorPreset
interface ColorPresetMapper { interface ColorPresetMapper {
suspend fun toColorPresetEntity(colorPreset: ColorPreset, order: Int): ColorPresetEntity fun toColorPresetEntity(colorPreset: ColorPreset, order: Int): ColorPresetEntity
fun toColorPreset(colorPresetEntity: ColorPresetEntity): ColorPreset
suspend fun toColorPreset(colorPresetEntity: ColorPresetEntity): ColorPreset
} }

View file

@ -8,11 +8,11 @@ package ua.acclorite.book_story.data.mapper.color_preset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.model.reader.ColorPreset
import javax.inject.Inject import javax.inject.Inject
class ColorPresetMapperImpl @Inject constructor() : ColorPresetMapper { class ColorPresetMapperImpl @Inject constructor() : ColorPresetMapper {
override suspend fun toColorPresetEntity( override fun toColorPresetEntity(
colorPreset: ColorPreset, colorPreset: ColorPreset,
order: Int order: Int
): ColorPresetEntity { ): ColorPresetEntity {
@ -27,7 +27,7 @@ class ColorPresetMapperImpl @Inject constructor() : ColorPresetMapper {
) )
} }
override suspend fun toColorPreset(colorPresetEntity: ColorPresetEntity): ColorPreset { override fun toColorPreset(colorPresetEntity: ColorPresetEntity): ColorPreset {
return ColorPreset( return ColorPreset(
id = colorPresetEntity.id!!, id = colorPresetEntity.id!!,
name = colorPresetEntity.name, name = colorPresetEntity.name,

View file

@ -0,0 +1,15 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.mapper.file
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.domain.model.file.File
interface FileMapper {
fun toCachedFile(file: File): CachedFile
fun toFile(cachedFile: CachedFile): File
}

View file

@ -0,0 +1,43 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.mapper.file
import android.app.Application
import androidx.core.net.toUri
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.file.CachedFileCompat
import ua.acclorite.book_story.domain.model.file.File
import javax.inject.Inject
class FileMapperImpl @Inject constructor(
private val application: Application
) : FileMapper {
override fun toCachedFile(file: File): CachedFile {
return CachedFileCompat.fromUri(
context = application,
uri = file.uri.toUri(),
builder = CachedFileCompat.build(
name = file.name,
path = file.path,
size = file.size,
lastModified = file.lastModified,
isDirectory = file.isDirectory
)
)
}
override fun toFile(cachedFile: CachedFile): File {
return File(
name = cachedFile.name,
uri = cachedFile.uri.toString(),
path = cachedFile.path,
size = cachedFile.size,
lastModified = cachedFile.lastModified,
isDirectory = cachedFile.isDirectory
)
}
}

View file

@ -7,7 +7,7 @@
package ua.acclorite.book_story.data.mapper.history package ua.acclorite.book_story.data.mapper.history
import ua.acclorite.book_story.data.local.dto.HistoryEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity
import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.model.history.History
interface HistoryMapper { interface HistoryMapper {
suspend fun toHistoryEntity(history: History): HistoryEntity suspend fun toHistoryEntity(history: History): HistoryEntity

View file

@ -7,7 +7,7 @@
package ua.acclorite.book_story.data.mapper.history package ua.acclorite.book_story.data.mapper.history
import ua.acclorite.book_story.data.local.dto.HistoryEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity
import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.model.history.History
import javax.inject.Inject import javax.inject.Inject
class HistoryMapperImpl @Inject constructor() : HistoryMapper { class HistoryMapperImpl @Inject constructor() : HistoryMapper {

View file

@ -4,10 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.library.book package ua.acclorite.book_story.data.model.common
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.library.BookWithCover
@Immutable @Immutable
sealed class NullableBook( sealed class NullableBook(

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.file package ua.acclorite.book_story.data.model.file
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
@ -168,7 +168,7 @@ class CachedFile(
} }
/** /**
* Copies the file to [context].cacheDir and provides [java.io.File]. * Copies the file to [context].cacheDir and provides [File].
* *
* @return null if the file is directory or failed * @return null if the file is directory or failed
*/ */

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.file package ua.acclorite.book_story.data.model.file
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.file package ua.acclorite.book_story.data.model.file
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri

View file

@ -4,10 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.library.book package ua.acclorite.book_story.data.model.library
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.core.CoverImage
import ua.acclorite.book_story.domain.model.library.Book
@Immutable @Immutable
data class BookWithCover( data class BookWithCover(

View file

@ -11,10 +11,10 @@ import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import ua.acclorite.book_story.domain.reader.ReaderText import ua.acclorite.book_story.core.helpers.clearAllMarkdown
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import ua.acclorite.book_story.core.helpers.clearMarkdown
import ua.acclorite.book_story.presentation.core.util.clearMarkdown import ua.acclorite.book_story.core.helpers.containsVisibleText
import ua.acclorite.book_story.presentation.core.util.containsVisibleText import ua.acclorite.book_story.domain.model.reader.ReaderText
import java.io.File import java.io.File
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile

View file

@ -6,8 +6,8 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.domain.library.book.BookWithCover import ua.acclorite.book_story.data.model.library.BookWithCover
interface FileParser { interface FileParser {

View file

@ -7,13 +7,13 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import android.util.Log import android.util.Log
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.epub.EpubFileParser import ua.acclorite.book_story.data.parser.epub.EpubFileParser
import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser
import ua.acclorite.book_story.data.parser.html.HtmlFileParser import ua.acclorite.book_story.data.parser.html.HtmlFileParser
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
import ua.acclorite.book_story.data.parser.txt.TxtFileParser import ua.acclorite.book_story.data.parser.txt.TxtFileParser
import ua.acclorite.book_story.domain.file.CachedFile
import ua.acclorite.book_story.domain.library.book.BookWithCover
import javax.inject.Inject import javax.inject.Inject
private const val FILE_PARSER = "File Parser" private const val FILE_PARSER = "File Parser"

View file

@ -23,7 +23,7 @@ import org.commonmark.node.Node
import org.commonmark.node.StrongEmphasis import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text import org.commonmark.node.Text
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import ua.acclorite.book_story.presentation.core.util.clearMarkdown import ua.acclorite.book_story.core.helpers.clearMarkdown
import javax.inject.Inject import javax.inject.Inject
class MarkdownParser @Inject constructor( class MarkdownParser @Inject constructor(

View file

@ -6,8 +6,8 @@
package ua.acclorite.book_story.data.parser package ua.acclorite.book_story.data.parser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.domain.reader.ReaderText import ua.acclorite.book_story.domain.model.reader.ReaderText
interface TextParser { interface TextParser {

View file

@ -9,13 +9,13 @@ package ua.acclorite.book_story.data.parser
import android.util.Log import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.parser.epub.EpubTextParser import ua.acclorite.book_story.data.parser.epub.EpubTextParser
import ua.acclorite.book_story.data.parser.html.HtmlTextParser import ua.acclorite.book_story.data.parser.html.HtmlTextParser
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
import ua.acclorite.book_story.data.parser.txt.TxtTextParser import ua.acclorite.book_story.data.parser.txt.TxtTextParser
import ua.acclorite.book_story.data.parser.xml.XmlTextParser import ua.acclorite.book_story.data.parser.xml.XmlTextParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText
import javax.inject.Inject import javax.inject.Inject
private const val TEXT_PARSER = "Text Parser" private const val TEXT_PARSER = "Text Parser"

View file

@ -12,11 +12,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jsoup.Jsoup import org.jsoup.Jsoup
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.ui.UIText
import java.io.File import java.io.File
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject

View file

@ -17,13 +17,13 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.Jsoup import org.jsoup.Jsoup
import ua.acclorite.book_story.core.data.ExtensionsData
import ua.acclorite.book_story.core.helpers.addAll
import ua.acclorite.book_story.core.helpers.containsVisibleText
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.DocumentParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.presentation.core.constants.provideImageExtensions
import ua.acclorite.book_story.presentation.core.util.addAll
import ua.acclorite.book_story.presentation.core.util.containsVisibleText
import java.io.File import java.io.File
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -60,7 +60,7 @@ class EpubTextParser @Inject constructor(
val chapterEntries = zip.getChapterEntries(opfEntry) val chapterEntries = zip.getChapterEntries(opfEntry)
val imageEntries = zip.entries().toList().filter { val imageEntries = zip.entries().toList().filter {
provideImageExtensions().any { format -> ExtensionsData.imageExtensions.any { format ->
it.name.endsWith(format, ignoreCase = true) it.name.endsWith(format, ignoreCase = true)
} }
} }

View file

@ -9,11 +9,11 @@ package ua.acclorite.book_story.data.parser.fb2
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.ui.UIText
import javax.inject.Inject import javax.inject.Inject
class Fb2FileParser @Inject constructor() : FileParser { class Fb2FileParser @Inject constructor() : FileParser {

View file

@ -9,11 +9,11 @@ package ua.acclorite.book_story.data.parser.html
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.ui.UIText
import javax.inject.Inject import javax.inject.Inject
class HtmlFileParser @Inject constructor() : FileParser { class HtmlFileParser @Inject constructor() : FileParser {

View file

@ -10,10 +10,10 @@ import android.util.Log
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.DocumentParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText
import javax.inject.Inject import javax.inject.Inject
private const val HTML_TAG = "HTML Parser" private const val HTML_TAG = "HTML Parser"

View file

@ -10,11 +10,11 @@ import android.app.Application
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import com.tom_roush.pdfbox.pdmodel.PDDocument import com.tom_roush.pdfbox.pdmodel.PDDocument
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.ui.UIText
import javax.inject.Inject import javax.inject.Inject
class PdfFileParser @Inject constructor( class PdfFileParser @Inject constructor(

View file

@ -12,11 +12,11 @@ import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import com.tom_roush.pdfbox.pdmodel.PDDocument import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.text.PDFTextStripper import com.tom_roush.pdfbox.text.PDFTextStripper
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.core.helpers.clearAllMarkdown
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.parser.MarkdownParser import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import javax.inject.Inject import javax.inject.Inject
private const val PDF_TAG = "PDF Parser" private const val PDF_TAG = "PDF Parser"

View file

@ -7,11 +7,11 @@
package ua.acclorite.book_story.data.parser.txt package ua.acclorite.book_story.data.parser.txt
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.core.ui.UIText
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.ui.UIText
import javax.inject.Inject import javax.inject.Inject
class TxtFileParser @Inject constructor() : FileParser { class TxtFileParser @Inject constructor() : FileParser {

View file

@ -10,11 +10,11 @@ import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.core.helpers.clearAllMarkdown
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.parser.MarkdownParser import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
import javax.inject.Inject import javax.inject.Inject
private const val TXT_TAG = "TXT Parser" private const val TXT_TAG = "TXT Parser"

View file

@ -10,10 +10,10 @@ import android.util.Log
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.DocumentParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.reader.ReaderText
import javax.inject.Inject import javax.inject.Inject
private const val XML_TAG = "XML Parser" private const val XML_TAG = "XML Parser"

View file

@ -6,399 +6,62 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import android.app.Application import ua.acclorite.book_story.core.CoverImage
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.file.CachedFileCompat import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.service.FileProvider
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val GET_TEXT = "GET TEXT, REPO"
private const val GET_BOOKS = "GET BOOKS, REPO"
private const val GET_BOOKS_BY_ID = "GET BOOKS, REPO"
private const val INSERT_BOOK = "INSERT BOOK, REPO"
private const val UPDATE_BOOK = "UPDATE BOOK, REPO"
private const val DELETE_BOOKS = "DELETE BOOKS, REPO"
private const val CAN_RESET_COVER = "CAN RESET COVER, REPO"
private const val RESET_COVER = "RESET COVER, REPO"
@Suppress("DEPRECATION")
@Singleton @Singleton
class BookRepositoryImpl @Inject constructor( class BookRepositoryImpl @Inject constructor(
private val application: Application,
private val database: BookDao, private val database: BookDao,
private val bookMapper: BookMapper, private val bookMapper: BookMapper,
private val fileParser: FileParser, private val fileParser: FileParser,
private val textParser: TextParser private val textParser: TextParser,
private val fileProvider: FileProvider
) : BookRepository { ) : BookRepository {
/** override suspend fun searchBooks(query: String): Result<List<Book>> = runCatching {
* Get all books matching [query] from database. database.searchBooks(query).map { bookMapper.toBook(it) }
* Empty [query] equals to all books. }
*/
override suspend fun getBooks(query: String): List<Book> {
Log.i(GET_BOOKS, "Searching for books with query: \"$query\".")
val books = database.searchBooks(query)
Log.i(GET_BOOKS, "Found ${books.size} books.") override suspend fun getBook(bookId: Int): Result<Book> = runCatching {
return books.map { entity -> database.findBookById(bookId).let {
val book = bookMapper.toBook(entity) if (it == null) throw NoSuchElementException("Couldn't get book [$bookId].")
val lastHistory = database.getLatestHistoryForBook( else bookMapper.toBook(it)
book.id
)
book.copy(
lastOpened = lastHistory?.time
)
} }
} }
/** override suspend fun getText(bookId: Int): Result<List<ReaderText>> {
* Get all books that match given [ids]. return getBook(bookId)
*/ .mapCatching { fileProvider.getFileFromBook(it).getOrThrow() }
override suspend fun getBooksById(ids: List<Int>): List<Book> { .mapCatching { textParser.parse(it) }
Log.i(GET_BOOKS_BY_ID, "Getting books with ids: $ids.") }
val books = database.findBooksById(ids)
return books.map { entity -> override suspend fun addBook(book: Book): Result<Unit> = runCatching {
val book = bookMapper.toBook(entity) database.insertBook(bookMapper.toBookEntity(book))
val lastHistory = database.getLatestHistoryForBook( }
book.id
)
book.copy( override suspend fun updateBook(book: Book): Result<Unit> = runCatching {
lastOpened = lastHistory?.time database.updateBook(bookMapper.toBookEntity(book)).also {
) if (it == 0) throw Exception("Could not update book in database.")
} }
} }
/** override suspend fun deleteBook(book: Book): Result<Unit> = runCatching {
* Loads text from the book. Already formatted. database.deleteBook(bookMapper.toBookEntity(book)).also {
*/ if (it == 0) throw Exception("Could not delete book in database.")
override suspend fun getBookText(bookId: Int): List<ReaderText> {
if (bookId == -1) return emptyList()
val book = database.findBookById(bookId)
val cachedFile = CachedFileCompat.fromFullPath(
context = application,
path = book.filePath,
builder = CachedFileCompat.build(
name = book.filePath.substringAfterLast(File.separator),
path = book.filePath,
isDirectory = false
)
)
if (cachedFile == null || !cachedFile.canAccess()) {
Log.e(GET_TEXT, "File [$bookId] does not exist")
return emptyList()
} }
val readerText = textParser.parse(cachedFile)
if (
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
Log.e(GET_TEXT, "Could not load text from [$bookId].")
return emptyList()
}
Log.i(GET_TEXT, "Successfully loaded text of [$bookId] with markdown.")
return readerText
} }
/** override suspend fun getDefaultCover(book: Book): Result<CoverImage?> = runCatching {
* Inserts book in database. return fileProvider.getFileFromBook(book)
* Creates covers folder, which contains cover. .mapCatching { fileParser.parse(it)?.coverImage }
*/
override suspend fun insertBook(
bookWithCover: BookWithCover
) {
Log.i(INSERT_BOOK, "Inserting ${bookWithCover.book.title}.")
val filesDir = application.filesDir
val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) {
Log.i(INSERT_BOOK, "Created covers folder.")
coversDir.mkdirs()
}
var coverUri = ""
if (bookWithCover.coverImage != null) {
try {
coverUri = "${UUID.randomUUID()}.webp"
val cover = File(coversDir, coverUri)
withContext(Dispatchers.IO) {
BufferedOutputStream(FileOutputStream(cover)).use { output ->
bookWithCover.coverImage
.copy(Bitmap.Config.RGB_565, false)
.compress(Bitmap.CompressFormat.WEBP, 20, output)
.let { success ->
if (success) return@let
throw Exception("Couldn't save cover image")
}
}
}
} catch (e: Exception) {
Log.e(INSERT_BOOK, "Could not save cover.")
coverUri = ""
e.printStackTrace()
}
}
val updatedBook = bookWithCover.book.copy(
coverImage = if (coverUri.isNotBlank()) {
Uri.fromFile(File("$coversDir/$coverUri"))
} else null
)
val bookToInsert = bookMapper.toBookEntity(updatedBook)
database.insertBook(bookToInsert)
Log.i(INSERT_BOOK, "Successfully inserted book.")
}
/**
* Update book without cover image.
*/
override suspend fun updateBook(book: Book) {
val entity = database.findBookById(book.id)
database.updateBooks(
listOf(
bookMapper.toBookEntity(
book.copy(
coverImage = entity.image?.toUri()
)
)
)
)
}
/**
* Update cover image of the book. Deletes old cover and replaces with new.
*/
override suspend fun updateCoverImageOfBook(
bookWithOldCover: Book,
newCoverImage: CoverImage?
) {
Log.i(UPDATE_BOOK, "Updating cover image: ${bookWithOldCover.title}.")
val book = database.findBookById(bookWithOldCover.id)
var uri: String? = null
val filesDir = application.filesDir
val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) {
Log.i(UPDATE_BOOK, "Created covers folder.")
coversDir.mkdirs()
}
if (newCoverImage != null) {
try {
uri = "${UUID.randomUUID()}.webp"
val cover = File(coversDir, uri)
withContext(Dispatchers.IO) {
BufferedOutputStream(FileOutputStream(cover)).use { output ->
newCoverImage
.copy(Bitmap.Config.RGB_565, false)
.compress(Bitmap.CompressFormat.WEBP, 20, output)
.let { success ->
if (success) return@let
throw Exception("Couldn't save cover image")
}
}
}
} catch (e: Exception) {
Log.e(UPDATE_BOOK, "Could not save new cover.")
e.printStackTrace()
return
}
}
if (book.image != null) {
try {
val fileToDelete = File(
"$coversDir${File.separator}${book.image.substringAfterLast(File.separator)}"
)
if (fileToDelete.exists()) {
fileToDelete.delete()
}
} catch (e: Exception) {
Log.e(UPDATE_BOOK, "Could not delete old cover.")
e.printStackTrace()
}
}
val newCoverImageUri = if (uri != null) {
Uri.fromFile(File("$coversDir/$uri"))
} else {
null
}
val bookWithNewCover = bookWithOldCover.copy(
coverImage = newCoverImageUri
)
database.updateBooks(
listOf(
bookMapper.toBookEntity(
bookWithNewCover
)
)
)
Log.i(UPDATE_BOOK, "Successfully updated cover image.")
}
/**
* Delete books.
* Also deletes cover image and text from internal storage.
*/
override suspend fun deleteBooks(books: List<Book>) {
Log.i(DELETE_BOOKS, "Deleting books.")
val filesDir = application.filesDir
val coversDir = File(filesDir, "covers")
if (!coversDir.exists()) {
Log.i(DELETE_BOOKS, "Created covers folder.")
coversDir.mkdirs()
}
database.deleteBooks(
books.map {
val book = database.findBookById(it.id)
if (book.image != null) {
try {
val fileToDelete = File(
"$coversDir${File.separator}${book.image.substringAfterLast(File.separator)}"
)
if (fileToDelete.exists()) {
fileToDelete.delete()
}
} catch (e: Exception) {
Log.e(DELETE_BOOKS, "Could not delete cover image.")
e.printStackTrace()
}
}
book
}
)
Log.i(DELETE_BOOKS, "Successfully deleted books.")
}
/**
* @return Whether can reset cover image (restore default).
*/
override suspend fun canResetCover(bookId: Int): Boolean {
val book = database.findBookById(bookId)
val cachedFile = CachedFileCompat.fromFullPath(
application,
book.filePath,
builder = CachedFileCompat.build(
name = book.filePath.substringAfterLast(File.separator),
path = book.filePath,
isDirectory = false
)
)
if (cachedFile == null || !cachedFile.canAccess()) {
return false
}
val defaultCoverUncompressed = fileParser.parse(cachedFile)?.coverImage
?: return false
if (book.image == null) {
Log.i(CAN_RESET_COVER, "Can reset cover image. (current is null)")
return true
}
val stream = ByteArrayOutputStream()
defaultCoverUncompressed.copy(Bitmap.Config.RGB_565, false).compress(
Bitmap.CompressFormat.WEBP,
20,
stream
)
val byteArray = stream.toByteArray()
val defaultCover = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val currentCover = try {
MediaStore.Images.Media.getBitmap(
application.contentResolver,
book.image.toUri()
)
} catch (e: Exception) {
Log.i(CAN_RESET_COVER, "Can reset cover image. (could not get current)")
e.printStackTrace()
return true
}
return !defaultCover.sameAs(currentCover)
}
/**
* Reset cover image to default.
* If there is no default cover, returns false.
*/
override suspend fun resetCoverImage(bookId: Int): Boolean {
if (!canResetCover(bookId)) {
Log.w(RESET_COVER, "Cannot reset cover image.")
return false
}
val book = database.findBookById(bookId)
val cachedFile = CachedFileCompat.fromFullPath(
application,
book.filePath,
builder = CachedFileCompat.build(
name = book.filePath.substringAfterLast(File.separator),
path = book.filePath,
isDirectory = false
)
)
if (cachedFile == null || !cachedFile.canAccess()) {
return false
}
val defaultCover = fileParser.parse(cachedFile)?.coverImage
?: return false
updateCoverImageOfBook(bookMapper.toBook(book), defaultCover)
Log.i(RESET_COVER, "Successfully reset cover image.")
return true
} }
} }

View file

@ -9,8 +9,8 @@ package ua.acclorite.book_story.data.repository
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.category.CategoryMapper import ua.acclorite.book_story.data.mapper.category.CategoryMapper
import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper
import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.library.category.CategorySort import ua.acclorite.book_story.domain.model.library.CategorySort
import ua.acclorite.book_story.domain.repository.CategoryRepository import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -22,7 +22,7 @@ class CategoryRepositoryImpl @Inject constructor(
private val categorySortMapper: CategorySortMapper, private val categorySortMapper: CategorySortMapper,
) : CategoryRepository { ) : CategoryRepository {
override suspend fun insertCategory(category: Category) { override suspend fun addCategory(category: Category): Result<Unit> = runCatching {
database.insertCategory( database.insertCategory(
categoryMapper.toCategoryEntity( categoryMapper.toCategoryEntity(
category.copy( category.copy(
@ -32,39 +32,41 @@ class CategoryRepositoryImpl @Inject constructor(
) )
} }
override suspend fun getCategories(): List<Category> { override suspend fun getCategories(): Result<List<Category>> = runCatching {
return database.getCategories().map { categoryMapper.toCategory(it) }.sortedBy { it.order } database.getCategories().map { categoryMapper.toCategory(it) }.sortedBy { it.order }
} }
override suspend fun updateCategoryTitle(id: Int, title: String) { override suspend fun updateCategory(category: Category): Result<Unit> = runCatching {
database.updateCategoryTitle( database.updateCategoryTitle(
id = id, id = category.id,
title = title title = category.title
) )
updateCategoriesOrder(getCategories()) updateOrder(getCategories().getOrThrow())
} }
override suspend fun updateCategoriesOrder(categories: List<Category>) { override suspend fun updateOrder(categories: List<Category>): Result<Unit> = runCatching {
categories.mapIndexed { index, category -> categories.forEachIndexed { index, category ->
database.updateCategoryOrder(category.id, index) database.updateCategoryOrder(category.id, index)
} }
} }
override suspend fun deleteCategory(category: Category) { override suspend fun deleteCategory(category: Category): Result<Unit> = runCatching {
database.deleteCategory(categoryMapper.toCategoryEntity(category)) database.deleteCategory(categoryMapper.toCategoryEntity(category))
database.deleteCategorySortEntity(category.id) database.deleteCategorySortEntity(category.id)
updateCategoriesOrder(getCategories()) updateOrder(getCategories().getOrThrow())
} }
override suspend fun updateCategorySortEntity(categorySort: CategorySort) { override suspend fun updateCategorySorting(
categorySort: CategorySort
): Result<Unit> = runCatching {
database.updateCategorySort( database.updateCategorySort(
categorySortMapper.toCategorySortEntity(categorySort) categorySortMapper.toCategorySortEntity(categorySort)
) )
} }
override suspend fun getCategorySortEntities(): List<CategorySort> { override suspend fun getCategorySorting(): Result<List<CategorySort>> = runCatching {
return database.getCategorySortEntities().map { database.getCategorySortEntities().map {
categorySortMapper.toCategorySort(it) categorySortMapper.toCategorySort(it)
} }
} }

View file

@ -8,7 +8,7 @@ package ua.acclorite.book_story.data.repository
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.model.reader.ColorPreset
import ua.acclorite.book_story.domain.repository.ColorPresetRepository import ua.acclorite.book_story.domain.repository.ColorPresetRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -20,64 +20,51 @@ import javax.inject.Singleton
@Singleton @Singleton
class ColorPresetRepositoryImpl @Inject constructor( class ColorPresetRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDao,
private val colorPresetMapper: ColorPresetMapper private val colorPresetMapper: ColorPresetMapper
) : ColorPresetRepository { ) : ColorPresetRepository {
/** override suspend fun getColorPresets(): Result<List<ColorPreset>> = runCatching {
* Update color preset. database.getColorPresets()
*/ .sortedBy { it.order }
override suspend fun updateColorPreset(colorPreset: ColorPreset) { .map { colorPresetMapper.toColorPreset(it) }
}
override suspend fun updateColorPreset(
colorPreset: ColorPreset
): Result<Unit> = runCatching {
database.updateColorPreset( database.updateColorPreset(
colorPresetMapper.toColorPresetEntity( colorPresetMapper.toColorPresetEntity(
colorPreset, colorPreset = colorPreset,
if (colorPreset.id != -1) database.getColorPresetOrder(colorPreset.id) order = if (colorPreset.id != -1) database.getColorPresetOrder(colorPreset.id)
else database.getColorPresetsSize() else database.getColorPresetsSize()
) )
) )
} }
/** override suspend fun selectColorPreset(
* Select color preset. Only one can be selected at time. colorPreset: ColorPreset
*/ ): Result<Unit> = runCatching {
override suspend fun selectColorPreset(colorPreset: ColorPreset) {
database.getColorPresets().map { database.getColorPresets().map {
it.copy( it.copy(isSelected = it.id == colorPreset.id)
isSelected = it.id == colorPreset.id
)
}.forEach { }.forEach {
database.updateColorPreset(it) database.updateColorPreset(it)
} }
} }
/** override suspend fun reorderColorPresets(
* Get all color presets. colorPresets: List<ColorPreset>
* Sorted by order (either manual or newest ones at the end). ): Result<Unit> = runCatching {
*/
override suspend fun getColorPresets(): List<ColorPreset> {
return database.getColorPresets()
.sortedBy { it.order }
.map { colorPresetMapper.toColorPreset(it) }
}
/**
* Reorder color presets.
* Changes the order of the color presets.
*/
override suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>) {
database.deleteColorPresets() database.deleteColorPresets()
colorPresets.forEachIndexed { index, colorPreset ->
orderedColorPresets.forEachIndexed { index, colorPreset ->
database.updateColorPreset( database.updateColorPreset(
colorPresetMapper.toColorPresetEntity(colorPreset, order = index) colorPresetMapper.toColorPresetEntity(colorPreset, order = index)
) )
} }
} }
/** override suspend fun deleteColorPreset(
* Delete color preset. colorPreset: ColorPreset
*/ ): Result<Unit> = runCatching {
override suspend fun deleteColorPreset(colorPreset: ColorPreset) {
database.deleteColorPreset( database.deleteColorPreset(
colorPresetMapper.toColorPresetEntity( colorPresetMapper.toColorPresetEntity(
colorPreset, -1 colorPreset, -1

View file

@ -6,22 +6,12 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import android.util.Log
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.domain.repository.DataStoreRepository import ua.acclorite.book_story.domain.repository.DataStoreRepository
import ua.acclorite.book_story.ui.main.MainState
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val GET_ALL_SETTINGS = "GET SETTINGS, REPO"
/** /**
* Data Store repository. * Data Store repository.
* Manages all [DataStore] related work. * Manages all [DataStore] related work.
@ -31,42 +21,20 @@ class DataStoreRepositoryImpl @Inject constructor(
private val dataStore: DataStore private val dataStore: DataStore
) : DataStoreRepository { ) : DataStoreRepository {
/** override suspend fun <T> putPreference(
* Puts DataStore constant to [DataStore]. key: Preferences.Key<T>,
*/ value: T
override suspend fun <T> putDataToDataStore(key: Preferences.Key<T>, value: T) { ): Result<Unit> = runCatching {
dataStore.putData(key, value) dataStore.putData(key, value)
} }
/** override suspend fun <T> getPreference(
* Gets all settings from DataStore and returns [MainState]. key: Preferences.Key<T>
*/ ): Result<T> = runCatching {
override suspend fun getAllSettings(): MainState { dataStore.getNullableData(key) ?: throw NoSuchElementException("Could not get preference.")
Log.i(GET_ALL_SETTINGS, "Getting all settings.") }
val result = CompletableDeferred<MainState>()
withContext(Dispatchers.Default) { override suspend fun getAllPreferences(): Result<Set<Preferences.Key<*>>> = runCatching {
val keys = dataStore.getAllData() dataStore.getAllData() ?: throw NoSuchElementException("Could not get all preferences.")
val data = ConcurrentHashMap<String, Any>()
Log.i(GET_ALL_SETTINGS, "Got ${keys?.size} settings keys.")
val jobs = keys?.map { key ->
async {
val nullableData = dataStore.getNullableData(key)
if (nullableData == null) {
data.remove(key.name)
} else {
data[key.name] = nullableData
}
}
}
jobs?.awaitAll()
result.complete(MainState.initialize(data))
}
return result.await()
} }
} }

View file

@ -6,149 +6,75 @@
package ua.acclorite.book_story.data.repository package ua.acclorite.book_story.data.repository
import android.app.Application import ua.acclorite.book_story.core.data.ExtensionsData
import android.util.Log
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.file.FileMapper
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.browse.file.SelectableFile import ua.acclorite.book_story.domain.model.file.File
import ua.acclorite.book_story.domain.file.CachedFile
import ua.acclorite.book_story.domain.file.CachedFileCompat
import ua.acclorite.book_story.domain.library.book.NullableBook
import ua.acclorite.book_story.domain.library.book.NullableBook.NotNull
import ua.acclorite.book_story.domain.library.book.NullableBook.Null
import ua.acclorite.book_story.domain.repository.FileSystemRepository import ua.acclorite.book_story.domain.repository.FileSystemRepository
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.domain.service.FileProvider
import ua.acclorite.book_story.presentation.core.constants.provideExtensions
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val GET_BOOK_FROM_FILE = "BOOK FROM FILE, REPO"
private const val GET_FILES = "FILES, REPO"
/** /**
* File System repository. * File System repository.
* Manages all File System related work. * Manages all File System related work.
*/ */
@Singleton @Singleton
class FileSystemRepositoryImpl @Inject constructor( class FileSystemRepositoryImpl @Inject constructor(
private val application: Application,
private val database: BookDao, private val database: BookDao,
private val fileParser: FileParser private val fileMapper: FileMapper,
private val fileParser: FileParser,
private val fileProvider: FileProvider
) : FileSystemRepository { ) : FileSystemRepository {
/** override suspend fun searchFiles(query: String): Result<List<File>> {
* Gets all matching files from device. return fileProvider.getStorageFiles().mapCatching { storages ->
* Filters by [query] and sorts out not supported file formats and already added files. val existingFiles = database.searchBooks("").map { it.filePath }
*/
override suspend fun getFiles(query: String): List<SelectableFile> {
Log.i(GET_FILES, "Getting files from device, query: \"$query\".")
val existingPaths = database storages.map { storage ->
.searchBooks("") storage.getFilesFromStorage(
.map { it.filePath } query = query,
val supportedExtensions = provideExtensions() existingFiles = existingFiles
/**
* Verify that [CachedFile] is valid and can be shown correctly.
*/
fun CachedFile.isValid(): Boolean {
// First: Ensuring supported extension
supportedExtensions.any { ext ->
name.endsWith(ext, ignoreCase = true)
}.let { if (!it) return false }
// Second: Ensuring query to match
if (query.isNotBlank()) {
name.contains(query.trim(), ignoreCase = true).let {
if (!it) return false
}
}
// Third: Ensuring that a file is not added already
existingPaths.none { existingPath ->
existingPath.equals(path, ignoreCase = true)
}.let { if (!it) return false }
return true
}
fun CachedFile.getSelectableFilesFromStorage(): List<SelectableFile> {
val selectableFiles = mutableListOf<SelectableFile>()
walk { file ->
if (!file.isValid()) return@walk
selectableFiles.add(
SelectableFile(
data = file,
selected = false
)
) )
} }.flatten()
return selectableFiles
}
fun getAllStorages(): List<CachedFile> {
return application.contentResolver.persistedUriPermissions.mapNotNull { permission ->
val storage = CachedFileCompat.fromUri(
application,
permission.uri,
builder = CachedFileCompat.build(
name = UUID.randomUUID().toString(),
size = 0,
lastModified = 0
)
)
if (!storage.isDirectory) return@mapNotNull null
storage
}.let { storages ->
storages.filter { storage ->
storages.none {
it.path != storage.path && storage.path.startsWith(
it.path,
ignoreCase = true
)
}
}
}
}
return try {
val storages = getAllStorages()
val files = mutableListOf<SelectableFile>()
for (storage in storages) {
files.addAll(storage.getSelectableFilesFromStorage())
}
Log.i(GET_FILES, "Successfully got all matching files.")
files
} catch (e: Exception) {
e.printStackTrace()
Log.e(GET_FILES, "Couldn't get all matching files.")
emptyList()
} }
} }
/** private fun CachedFile.isValid(
* Gets book from given file. If error happened, returns [NullableBook.Null]. query: String,
*/ existingFiles: List<String>
override suspend fun getBookFromFile(cachedFile: CachedFile): NullableBook { ): Boolean {
val parsedBook = fileParser.parse(cachedFile) if (
if (parsedBook == null) { ExtensionsData.fileExtensions.none {
Log.e(GET_BOOK_FROM_FILE, "Parsed file(${cachedFile.name}) is null.") name.endsWith(
return Null( it,
cachedFile.name, ignoreCase = true
UIText.StringResource(R.string.error_something_went_wrong) )
) }
} ) return false
if (query.isNotBlank() && !name.contains(query.trim(), ignoreCase = true)) return false
if (existingFiles.any { it.equals(path, ignoreCase = true) }) return false
return true
}
Log.i(GET_BOOK_FROM_FILE, "Successfully got book from file.") private fun CachedFile.getFilesFromStorage(
return NotNull(bookWithCover = parsedBook) query: String,
existingFiles: List<String>
): List<File> {
val files = mutableListOf<File>()
walk { cachedFile ->
if (cachedFile.isValid(query, existingFiles)) {
files.add(fileMapper.toFile(cachedFile))
}
}
return files
}
override suspend fun getBookFromFile(file: File): Result<BookWithCover> = runCatching {
fileParser.parse(fileMapper.toCachedFile(file))
?: throw Exception("Could not parse ${file.name}.")
} }
} }

View file

@ -8,78 +8,47 @@ package ua.acclorite.book_story.data.repository
import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.model.history.History
import ua.acclorite.book_story.domain.repository.HistoryRepository import ua.acclorite.book_story.domain.repository.HistoryRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
/**
* History repository.
* Manages all [History] related work.
*/
@Singleton @Singleton
class HistoryRepositoryImpl @Inject constructor( class HistoryRepositoryImpl @Inject constructor(
private val database: BookDao, private val database: BookDao,
private val historyMapper: HistoryMapper
private val historyMapper: HistoryMapper,
) : HistoryRepository { ) : HistoryRepository {
/** override suspend fun getHistoryForBook(bookId: Int): Result<History> = runCatching {
* Insert history in database. database.getHistoryForBook(bookId).let {
*/ if (it == null) throw NoSuchElementException("Could not get history from [$bookId].")
override suspend fun insertHistory(history: History) { else historyMapper.toHistory(it)
database.insertHistory(
listOf(
historyMapper.toHistoryEntity(
history
)
)
)
}
/**
* Get all history from database.
*/
override suspend fun getHistory(): List<History> {
return database.getHistory().map {
historyMapper.toHistory(
it
)
} }
} }
/** override suspend fun addHistory(history: History): Result<Unit> = runCatching {
* Get latest history of the matching [bookId]. database.insertHistory(historyMapper.toHistoryEntity(history))
*/
override suspend fun getLatestBookHistory(bookId: Int): History? {
val history = database.getLatestHistoryForBook(bookId)
return history?.let { historyMapper.toHistory(it) }
} }
/** override suspend fun getHistory(): Result<List<History>> = runCatching {
* Delete whole history. database.getHistory().map { historyMapper.toHistory(it) }
*/
override suspend fun deleteWholeHistory() {
database.deleteWholeHistory()
} }
/** override suspend fun deleteWholeHistory(): Result<Unit> = runCatching {
* Delete all history of the matching [bookId]. database.deleteWholeHistory().also {
*/ if (it == 0) throw Exception("Could not delete whole history in database.")
override suspend fun deleteBookHistory(bookId: Int) { }
database.deleteBookHistory(bookId)
} }
/** override suspend fun deleteHistoryForBook(bookId: Int): Result<Unit> = runCatching {
* Delete specific history item. database.deleteHistoryForBook(bookId = bookId).also {
*/ if (it == 0) throw Exception("Could not delete history for book [$bookId] in database.")
override suspend fun deleteHistory(history: History) { }
database.deleteHistory( }
listOf(
historyMapper.toHistoryEntity( override suspend fun deleteHistory(history: History): Result<Unit> = runCatching {
history database.deleteHistory(historyMapper.toHistoryEntity(history)).also {
) if (it == 0) throw Exception("Could not delete history in database.")
) }
)
} }
} }

View file

@ -8,45 +8,27 @@ package ua.acclorite.book_story.data.repository
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri import androidx.core.net.toUri
import android.util.Log
import ua.acclorite.book_story.domain.repository.PermissionRepository import ua.acclorite.book_story.domain.repository.PermissionRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private const val GRANT_URI = "GRANT URI PERM, REPO"
private const val RELEASE_URI = "RELEASE URI PERM, REPO"
@Singleton @Singleton
class PermissionRepositoryImpl @Inject constructor( class PermissionRepositoryImpl @Inject constructor(
private val application: Application private val application: Application
) : PermissionRepository { ) : PermissionRepository {
override suspend fun grantPersistableUriPermission(uri: Uri) { override suspend fun grantPersistableUriPermission(uri: String): Result<Unit> = runCatching {
Log.i(GRANT_URI, "Granting persistable uri permission to \"${uri.path}\" URI.") application.contentResolver.takePersistableUriPermission(
uri.toUri(),
try { Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
application.contentResolver.takePersistableUriPermission( )
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} catch (e: Exception) {
e.printStackTrace()
Log.e(GRANT_URI, "Could not grant URI permission.")
}
} }
override suspend fun releasePersistableUriPermission(uri: Uri) { override suspend fun releasePersistableUriPermission(uri: String): Result<Unit> = runCatching {
Log.i(RELEASE_URI, "Releasing persistable uri permission from \"${uri.path}\" URI.") application.contentResolver.releasePersistableUriPermission(
uri.toUri(),
try { Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
application.contentResolver.releasePersistableUriPermission( )
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} catch (e: Exception) {
e.printStackTrace()
Log.w(RELEASE_URI, "No granted URI permission found.")
}
} }
} }

View file

@ -0,0 +1,68 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.service
import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.net.toFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ua.acclorite.book_story.core.CoverImage
import ua.acclorite.book_story.domain.service.CoverImageHandler
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
import javax.inject.Inject
class CoverImageHandlerImpl @Inject constructor(
application: Application
) : CoverImageHandler {
private val filesDir: File = application.filesDir
private val coversDir = File(filesDir, "covers")
override suspend fun saveCover(coverImage: CoverImage): Result<File> = runCatching {
if (!coversDir.exists()) {
coversDir.mkdirs()
}
val coverUri = "${UUID.randomUUID()}.webp"
val cover = File(coversDir, coverUri)
withContext(Dispatchers.IO) {
BufferedOutputStream(FileOutputStream(cover)).use { output ->
coverImage
.copy(Bitmap.Config.RGB_565, false)
.compress(Bitmap.CompressFormat.WEBP, 20, output)
.let { success ->
if (success) return@let
throw Exception("Couldn't save cover image.")
}
}
}
cover
}
override suspend fun deleteCover(coverImage: Uri): Result<Unit> = runCatching {
coverImage.toFile().apply {
if (exists() && !delete()) throw Exception("Couldn't delete cover image.")
}
}
override suspend fun compressCover(coverImage: CoverImage): Result<CoverImage> = runCatching {
val stream = ByteArrayOutputStream()
coverImage.copy(Bitmap.Config.RGB_565, false)
.compress(Bitmap.CompressFormat.WEBP, 20, stream)
val byteArray = stream.toByteArray()
BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
}
}

View file

@ -0,0 +1,58 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.data.service
import android.app.Application
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.data.model.file.CachedFileCompat
import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.service.FileProvider
import javax.inject.Inject
class FileProviderImpl @Inject constructor(
private val application: Application
) : FileProvider {
override fun getFileFromBook(book: Book): Result<CachedFile> = runCatching {
CachedFileCompat.fromFullPath(
context = application,
path = book.filePath,
builder = CachedFileCompat.build(
name = book.filePath.substringAfterLast(java.io.File.separator),
path = book.filePath,
isDirectory = false
)
).let { file ->
if (file == null || !file.canAccess()) {
throw NoSuchElementException("Could not load CachedFile.")
}
file
}
}
override fun getStorageFiles(): Result<List<CachedFile>> = runCatching {
application.contentResolver.persistedUriPermissions.mapNotNull { permission ->
val storage = CachedFileCompat.fromUri(
application,
permission.uri
)
if (!storage.isDirectory) return@mapNotNull null
storage
}.let { storages ->
storages.filter { storage ->
storages.none {
it.path != storage.path && storage.path.startsWith(
it.path,
ignoreCase = true
)
}
}
}
}
}

View file

@ -1,17 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.browse.file
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.file.CachedFile
import ua.acclorite.book_story.domain.util.Selected
@Immutable
data class SelectableFile(
val data: CachedFile,
val selected: Selected
)

View file

@ -0,0 +1,19 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.model.file
import androidx.compose.runtime.Immutable
@Immutable
data class File(
val name: String,
val uri: String,
val path: String,
val size: Long,
val lastModified: Long,
val isDirectory: Boolean
)

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.history package ua.acclorite.book_story.domain.model.history
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.model.library.Book
@Immutable @Immutable
data class History( data class History(

View file

@ -4,13 +4,13 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.library.book package ua.acclorite.book_story.domain.model.library
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.core.ui.UIText
@Parcelize @Parcelize
@Immutable @Immutable
@ -30,4 +30,20 @@ data class Book(
val lastOpened: Long?, val lastOpened: Long?,
val categories: List<Int> val categories: List<Int>
) : Parcelable ) : Parcelable {
companion object {
val default = Book(
id = -1,
title = "",
author = UIText.StringValue(""),
description = null,
filePath = "",
coverImage = null,
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
lastOpened = null,
categories = emptyList()
)
}
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.library.category package ua.acclorite.book_story.domain.model.library
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.library.category package ua.acclorite.book_story.domain.model.library
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.library.display.LibrarySortOrder import ua.acclorite.book_story.presentation.library.model.LibrarySortOrder
@Immutable @Immutable
data class CategorySort( data class CategorySort(

View file

@ -0,0 +1,30 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.model.reader
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.core.Selected
@Immutable
data class ColorPreset(
val id: Int,
val name: String?,
val backgroundColor: Color,
val fontColor: Color,
val isSelected: Selected
) {
companion object {
val default = ColorPreset(
id = -1,
name = null,
backgroundColor = Color(0xFFFAF8FF), // Blue Light Surface (hardcoded)
fontColor = Color(0xFF44464F), // Blue Light OnSurfaceVariant (hardcoded)
isSelected = false
)
}
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package ua.acclorite.book_story.domain.reader package ua.acclorite.book_story.domain.model.reader
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap

View file

@ -1,81 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.navigator
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@HiltViewModel(assistedFactory = Navigator.Factory::class)
class Navigator @AssistedInject constructor(
private val savedStateHandle: SavedStateHandle,
@Assisted private val initialScreen: Screen
) : ViewModel() {
val items = savedStateHandle.getStateFlow("items", mutableListOf(initialScreen))
private fun StateFlow<MutableList<Screen>>.removeLast() {
savedStateHandle["items"] = value.dropLast(1)
}
private fun StateFlow<MutableList<Screen>>.add(item: Screen) {
savedStateHandle["items"] = value + item
}
val lastItem = items.map {
it.lastOrNull() ?: initialScreen
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = initialScreen
)
val lastEvent = savedStateHandle.getStateFlow("stack_event", StackEvent.Default)
private fun changeStackEvent(stackEvent: StackEvent) {
savedStateHandle["stack_event"] = stackEvent
}
fun push(
targetScreen: Screen,
popping: Boolean = false,
saveInBackStack: Boolean = true
) {
if (lastItem.value::class == targetScreen::class) return
if (!saveInBackStack) items.removeLast()
changeStackEvent(
if (popping) StackEvent.Pop
else StackEvent.Default
)
if (lastItem.value::class == targetScreen::class) items.removeLast()
items.add(targetScreen)
}
fun pop(popping: Boolean = true) {
if (items.value.count() > 1) {
changeStackEvent(
if (popping) StackEvent.Pop
else StackEvent.Default
)
items.removeLast()
}
}
@AssistedFactory
interface Factory {
fun create(startScreen: Screen): Navigator
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.reader
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.domain.util.Selected
@Immutable
data class ColorPreset(
val id: Int,
val name: String?,
val backgroundColor: Color,
val fontColor: Color,
val isSelected: Selected
)

View file

@ -6,47 +6,36 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.core.CoverImage
import ua.acclorite.book_story.domain.library.book.BookWithCover import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.reader.ReaderText import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.util.CoverImage
interface BookRepository { interface BookRepository {
suspend fun searchBooks(
suspend fun getBooks(
query: String query: String
): List<Book> ): Result<List<Book>>
suspend fun getBooksById( suspend fun getBook(
ids: List<Int>
): List<Book>
suspend fun getBookText(
bookId: Int bookId: Int
): List<ReaderText> ): Result<Book>
suspend fun insertBook( suspend fun getText(
bookWithCover: BookWithCover bookId: Int
) ): Result<List<ReaderText>>
suspend fun addBook(
book: Book
): Result<Unit>
suspend fun updateBook( suspend fun updateBook(
book: Book book: Book
) ): Result<Unit>
suspend fun updateCoverImageOfBook( suspend fun deleteBook(
bookWithOldCover: Book, book: Book
newCoverImage: CoverImage? ): Result<Unit>
)
suspend fun deleteBooks( suspend fun getDefaultCover(
books: List<Book> book: Book
) ): Result<CoverImage?>
suspend fun canResetCover(
bookId: Int
): Boolean
suspend fun resetCoverImage(
bookId: Int
): Boolean
} }

View file

@ -6,33 +6,31 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.library.category.CategorySort import ua.acclorite.book_story.domain.model.library.CategorySort
interface CategoryRepository { interface CategoryRepository {
suspend fun addCategory(
suspend fun insertCategory(
category: Category category: Category
) ): Result<Unit>
suspend fun getCategories(): List<Category> suspend fun getCategories(): Result<List<Category>>
suspend fun updateCategoryTitle( suspend fun updateCategory(
id: Int, category: Category
title: String ): Result<Unit>
)
suspend fun updateCategoriesOrder( suspend fun updateOrder(
categories: List<Category> categories: List<Category>
) ): Result<Unit>
suspend fun deleteCategory( suspend fun deleteCategory(
category: Category category: Category
) ): Result<Unit>
suspend fun updateCategorySortEntity( suspend fun updateCategorySorting(
categorySort: CategorySort categorySort: CategorySort
) ): Result<Unit>
suspend fun getCategorySortEntities(): List<CategorySort> suspend fun getCategorySorting(): Result<List<CategorySort>>
} }

View file

@ -6,25 +6,24 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.model.reader.ColorPreset
interface ColorPresetRepository { interface ColorPresetRepository {
suspend fun getColorPresets(): Result<List<ColorPreset>>
suspend fun updateColorPreset( suspend fun updateColorPreset(
colorPreset: ColorPreset colorPreset: ColorPreset
) ): Result<Unit>
suspend fun selectColorPreset( suspend fun selectColorPreset(
colorPreset: ColorPreset colorPreset: ColorPreset
) ): Result<Unit>
suspend fun getColorPresets(): List<ColorPreset>
suspend fun reorderColorPresets( suspend fun reorderColorPresets(
orderedColorPresets: List<ColorPreset> colorPresets: List<ColorPreset>
) ): Result<Unit>
suspend fun deleteColorPreset( suspend fun deleteColorPreset(
colorPreset: ColorPreset colorPreset: ColorPreset
) ): Result<Unit>
} }

View file

@ -7,14 +7,16 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import ua.acclorite.book_story.ui.main.MainState
interface DataStoreRepository { interface DataStoreRepository {
suspend fun <T> putPreference(
suspend fun <T> putDataToDataStore(
key: Preferences.Key<T>, key: Preferences.Key<T>,
value: T value: T
) ): Result<Unit>
suspend fun getAllSettings(): MainState suspend fun <T> getPreference(
key: Preferences.Key<T>,
): Result<T>
suspend fun getAllPreferences(): Result<Set<Preferences.Key<*>>>
} }

View file

@ -6,17 +6,15 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.browse.file.SelectableFile import ua.acclorite.book_story.data.model.library.BookWithCover
import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.model.file.File
import ua.acclorite.book_story.domain.library.book.NullableBook
interface FileSystemRepository { interface FileSystemRepository {
suspend fun searchFiles(
suspend fun getFiles(
query: String = "" query: String = ""
): List<SelectableFile> ): Result<List<File>>
suspend fun getBookFromFile( suspend fun getBookFromFile(
cachedFile: CachedFile file: File
): NullableBook ): Result<BookWithCover>
} }

View file

@ -6,27 +6,26 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import ua.acclorite.book_story.domain.history.History import ua.acclorite.book_story.domain.model.history.History
interface HistoryRepository { interface HistoryRepository {
suspend fun getHistoryForBook(
bookId: Int
): Result<History>
suspend fun insertHistory( suspend fun addHistory(
history: History history: History
) ): Result<Unit>
suspend fun getHistory(): List<History> suspend fun getHistory(): Result<List<History>>
suspend fun getLatestBookHistory( suspend fun deleteWholeHistory(): Result<Unit>
suspend fun deleteHistoryForBook(
bookId: Int bookId: Int
): History? ): Result<Unit>
suspend fun deleteWholeHistory()
suspend fun deleteBookHistory(
bookId: Int
)
suspend fun deleteHistory( suspend fun deleteHistory(
history: History history: History
) ): Result<Unit>
} }

View file

@ -6,15 +6,12 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import android.net.Uri
interface PermissionRepository { interface PermissionRepository {
suspend fun grantPersistableUriPermission( suspend fun grantPersistableUriPermission(
uri: Uri uri: String
) ): Result<Unit>
suspend fun releasePersistableUriPermission( suspend fun releasePersistableUriPermission(
uri: Uri uri: String
) ): Result<Unit>
} }

View file

@ -0,0 +1,17 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.service
import android.net.Uri
import ua.acclorite.book_story.core.CoverImage
import java.io.File
interface CoverImageHandler {
suspend fun saveCover(coverImage: CoverImage): Result<File>
suspend fun deleteCover(coverImage: Uri): Result<Unit>
suspend fun compressCover(coverImage: CoverImage): Result<CoverImage>
}

View file

@ -0,0 +1,15 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.service
import ua.acclorite.book_story.data.model.file.CachedFile
import ua.acclorite.book_story.domain.model.library.Book
interface FileProvider {
fun getFileFromBook(book: Book): Result<CachedFile>
fun getStorageFiles(): Result<List<CachedFile>>
}

View file

@ -0,0 +1,49 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import androidx.core.net.toUri
import ua.acclorite.book_story.core.CoverImage
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.core.log.logW
import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.service.CoverImageHandler
import javax.inject.Inject
class AddBookUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val coverImageHandler: CoverImageHandler
) {
suspend operator fun invoke(book: Book, coverImage: CoverImage?) {
logI("Inserting [${book.title}].")
val coverImageUri = coverImage?.let {
coverImageHandler.saveCover(it).fold(
onSuccess = {
logI("Successfully saved cover image of [${book.title}].")
it.toUri()
},
onFailure = {
logW("Could not save cover image with error: ${it.message}")
null
}
)
}
bookRepository.addBook(book = book.copy(coverImage = coverImageUri)).fold(
onSuccess = {
logI("Successfully inserted [${book.title}].")
},
onFailure = {
logE("Could not insert [${book.title}] with error: ${it.message}")
}
)
}
}

View file

@ -1,19 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class CanResetCover @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(bookId: Int): Boolean {
return repository.canResetCover(bookId)
}
}

View file

@ -0,0 +1,57 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import android.app.Application
import android.graphics.BitmapFactory
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.service.CoverImageHandler
import javax.inject.Inject
class CanResetCoverImageUseCase @Inject constructor(
private val application: Application,
private val bookRepository: BookRepository,
private val coverImageHandler: CoverImageHandler
) {
suspend operator fun invoke(bookId: Int): Boolean {
logI("Checking if can reset cover image of [$bookId].")
bookRepository.getBook(bookId).mapCatching { book ->
// Getting default cover image
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
if (defaultCoverImage == null) {
return@mapCatching true
}
// Return true if current cover is null (and default is not)
if (book.coverImage == null) {
return@mapCatching true
}
// Getting compressed cover images
val compressedDefaultCoverImage =
coverImageHandler.compressCover(defaultCoverImage).getOrThrow()
val compressedCoverImage = application.contentResolver
.openInputStream(book.coverImage)
?.use { BitmapFactory.decodeStream(it) }
!compressedDefaultCoverImage.sameAs(compressedCoverImage)
}.fold(
onSuccess = {
logI("Can reset cover image of [$bookId]: $it.")
return it
},
onFailure = {
logE("Could not check if can reset cover image of [$bookId] with error: ${it.message}")
return false
}
)
}
}

View file

@ -0,0 +1,47 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.core.log.logW
import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.repository.HistoryRepository
import ua.acclorite.book_story.domain.service.CoverImageHandler
import javax.inject.Inject
class DeleteBookUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val historyRepository: HistoryRepository,
private val coverImageHandler: CoverImageHandler
) {
suspend operator fun invoke(book: Book) {
logI("Deleting [${book.title}].")
// Deleting cover image
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
logW("Could not delete cover image with error: ${it.message}")
}
// Deleting history
historyRepository.deleteHistoryForBook(bookId = book.id).onFailure {
logW("Could not delete history for [${book.title}] with error: ${it.message}")
}
// Deleting book
bookRepository.deleteBook(book).fold(
onSuccess = {
logI("Successfully deleted [${book.title}].")
},
onFailure = {
logE("Could not delete [${book.title}] with error: ${it.message}")
}
)
}
}

View file

@ -1,25 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.repository.HistoryRepository
import javax.inject.Inject
class DeleteBooks @Inject constructor(
private val bookRepository: BookRepository,
private val historyRepository: HistoryRepository
) {
suspend fun execute(books: List<Book>) {
bookRepository.deleteBooks(books)
books.forEach {
historyRepository.deleteBookHistory(it.id)
}
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetBookById @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(id: Int): Book? {
return repository.getBooksById(listOf(id)).firstOrNull()
}
}

View file

@ -0,0 +1,39 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.repository.HistoryRepository
import javax.inject.Inject
class GetBookUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val historyRepository: HistoryRepository
) {
suspend operator fun invoke(bookId: Int): Book? {
logI("Getting book: [$bookId].")
bookRepository.getBook(bookId).fold(
onSuccess = { book ->
val history = historyRepository.getHistoryForBook(book.id).getOrNull()
logI("Successfully got book: [$bookId].")
return book.copy(
lastOpened = history?.time
)
},
onFailure = {
logE("Could not get book [$bookId] with error: ${it.message}")
return null
}
)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetBooks @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(query: String): List<Book> {
return repository.getBooks(query)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetBooksById @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(ids: List<Int>): List<Book> {
return repository.getBooksById(ids)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.reader.ReaderText
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetText @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(bookId: Int): List<ReaderText> {
return repository.getBookText(bookId = bookId)
}
}

View file

@ -0,0 +1,43 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.reader.ReaderText
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetTextUseCase @Inject constructor(
private val bookRepository: BookRepository
) {
suspend operator fun invoke(bookId: Int): List<ReaderText> {
logI("Getting text from: [$bookId].")
if (bookId == -1) return emptyList()
bookRepository.getText(bookId).mapCatching { readerText ->
if (
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
) {
throw Exception("ReaderText is empty.")
}
readerText
}.fold(
onSuccess = {
logI("Successfully loaded text from [$bookId] with markdown.")
return it
},
onFailure = {
logE("Could not load text with exception: ${it.message}")
return emptyList()
}
)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.BookWithCover
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class InsertBook @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(bookWithCover: BookWithCover) {
repository.insertBook(bookWithCover)
}
}

View file

@ -1,19 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class ResetCoverImage @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(bookId: Int): Boolean {
return repository.resetCoverImage(bookId)
}
}

View file

@ -0,0 +1,56 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import androidx.core.net.toUri
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.core.log.logW
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.service.CoverImageHandler
import javax.inject.Inject
class ResetCoverImageUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val coverImageHandler: CoverImageHandler
) {
suspend operator fun invoke(bookId: Int): Boolean {
logI("Resetting cover image of [$bookId].")
bookRepository.getBook(bookId).mapCatching { book ->
// Getting default cover image
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
if (defaultCoverImage == null) {
throw NoSuchElementException("Could not find default cover image")
}
// Deleting old cover
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
logW("Could not delete old cover image with error: ${it.message}")
}
// Resetting cover
val newCoverImage = defaultCoverImage.let {
coverImageHandler.saveCover(it).getOrThrow()
}
book.copy(coverImage = newCoverImage.toUri())
}.mapCatching { newBook ->
bookRepository.updateBook(newBook).getOrThrow()
}.fold(
onSuccess = {
logI("Successfully reset cover image of [$bookId].")
return true
},
onFailure = {
logE("Could not reset cover image of [$bookId] with error: ${it.message}")
return false
}
)
}
}

View file

@ -0,0 +1,41 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.repository.HistoryRepository
import javax.inject.Inject
class SearchBooksUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val historyRepository: HistoryRepository
) {
suspend operator fun invoke(query: String): List<Book> {
logI("Searching for books with query: \"$query\".")
return bookRepository.searchBooks(query).fold(
onSuccess = { books ->
logI("Successfully found [${books.size}] books.")
books.map { book ->
val history = historyRepository.getHistoryForBook(book.id).getOrNull()
book.copy(
lastOpened = history?.time
)
}
},
onFailure = {
logE("Could not find books with error: ${it.message}")
emptyList()
}
)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class UpdateBook @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(book: Book) {
repository.updateBook(book)
}
}

View file

@ -0,0 +1,31 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class UpdateBookUseCase @Inject constructor(
private val bookRepository: BookRepository
) {
suspend operator fun invoke(book: Book) {
logI("Updating [${book.title}].")
bookRepository.updateBook(book).fold(
onSuccess = {
logI("Successfully updated [${book.title}].")
},
onFailure = {
logE("Could not update [${book.title}] with error: ${it.message}")
}
)
}
}

View file

@ -1,21 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject
class UpdateCoverImageOfBook @Inject constructor(
private val repository: BookRepository
) {
suspend fun execute(bookWithOldCover: Book, newCoverImage: CoverImage?) {
repository.updateCoverImageOfBook(bookWithOldCover, newCoverImage)
}
}

View file

@ -0,0 +1,49 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.book
import androidx.core.net.toUri
import ua.acclorite.book_story.core.CoverImage
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.core.log.logW
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.service.CoverImageHandler
import javax.inject.Inject
class UpdateCoverImageUseCase @Inject constructor(
private val bookRepository: BookRepository,
private val coverImageHandler: CoverImageHandler
) {
suspend operator fun invoke(bookId: Int, coverImage: CoverImage?) {
logI("Updating cover image of [$bookId].")
bookRepository.getBook(bookId).mapCatching { book ->
// Deleting old cover
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
logW("Could not delete old cover image with error: ${it.message}")
}
// Saving new cover
val newCoverImage = coverImage?.let {
coverImageHandler.saveCover(it).getOrThrow()
}
book.copy(coverImage = newCoverImage?.toUri())
}.mapCatching { newBook ->
bookRepository.updateBook(newBook).getOrThrow()
}.fold(
onSuccess = {
logI("Successfully updated cover image of [$bookId].")
},
onFailure = {
logE("Could not update cover image of [$bookId] with error: ${it.message}")
}
)
}
}

View file

@ -0,0 +1,31 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class AddCategoryUseCase @Inject constructor(
private val categoryRepository: CategoryRepository
) {
suspend operator fun invoke(title: String) {
logI("Inserting [${title}] category.")
categoryRepository.addCategory(Category(title = title)).fold(
onSuccess = {
logI("Successfully inserted [${title}] category.")
},
onFailure = {
logE("Could not insert [${title}] category with error: ${it.message}")
}
)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.domain.library.category.Category
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class DeleteCategory @Inject constructor(
private val repository: CategoryRepository
) {
suspend fun execute(category: Category) {
repository.deleteCategory(category)
}
}

View file

@ -0,0 +1,30 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class DeleteCategoryUseCase @Inject constructor(
private val categoryRepository: CategoryRepository
) {
suspend operator fun invoke(category: Category) {
logI("Deleting category [${category.id}].")
categoryRepository.deleteCategory(category).fold(
onSuccess = {
logI("Successfully deleted category [${category.id}].")
},
onFailure = {
logI("Could not delete category [${category.id}] with error: ${it.message}")
}
)
}
}

View file

@ -1,20 +0,0 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.domain.library.category.Category
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class GetCategories @Inject constructor(
private val repository: CategoryRepository
) {
suspend fun execute(): List<Category> {
return repository.getCategories()
}
}

View file

@ -0,0 +1,33 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2025 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package ua.acclorite.book_story.domain.use_case.category
import ua.acclorite.book_story.core.log.logE
import ua.acclorite.book_story.core.log.logI
import ua.acclorite.book_story.domain.model.library.Category
import ua.acclorite.book_story.domain.repository.CategoryRepository
import javax.inject.Inject
class GetCategoriesUseCase @Inject constructor(
private val categoryRepository: CategoryRepository
) {
suspend operator fun invoke(): List<Category> {
logI("Getting all categories.")
return categoryRepository.getCategories().fold(
onSuccess = {
logI("Successfully got [${it.size}] categories.")
it
},
onFailure = {
logE("Could not get categories with error: ${it.message}")
emptyList()
}
)
}
}

Some files were not shown because too many files have changed in this diff Show more