⚖️ Merge changes from "development"

Merge changes from another branch.
This commit is contained in:
Acclorite 2024-08-15 14:01:05 +03:00
commit 6ceb27254f
79 changed files with 512 additions and 490 deletions

View file

@ -39,12 +39,14 @@ Built using Jetpack Compose and Kotlin.
<img src="https://github.com/Acclorite/book-story/blob/master/assets/readme/book's-story.gif" width="35%">
<div>
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Library Dark + Frame.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Book-Info Dark + Frame.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Reader Dark + Frame.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Library Light + Frame.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Book-Info Light + Frame.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Reader Light + Frame.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="45%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png" width="45%" />
</div>
</br>
@ -117,6 +119,10 @@ _Don't see your favorite app store? Create an [Issue](https://github.com/Acclori
## 🤝 How to contribute
- **The most improtant thing is I do not want someone's code.** This project is maintaining only by me and I want it to be this way. Creating Pull Requests will not have much sense as they will probably be closed.
- **Place your suggestions on Issues
with [Suggestion Needed](https://github.com/Acclorite/book-story/labels/%F0%9F%93%9D%20Suggestion%20needed)
label.** When the issue has this label that means I seek new ideas on how to improve what I
already propose. Feel free to tell me what's on your mind!
- **Share your ideas and suggestions.** Your ideas and suggestions are always welcome, if you miss some feature or got an interesting idea, feel free to create a new [Issue](https://github.com/Acclorite/book-story/issues).
- **Report bugs**. Had a crash or something went wrong? Create a new [Issue](https://github.com/Acclorite/book-story/issues), the more information you can give the better it is.
- **Enjoy the app**. Your best contribution is using my app and enjoying it!

View file

@ -137,9 +137,6 @@ dependencies {
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
// Epub parser
implementation("com.positiondev.epublib:epublib-core:3.1") {
exclude("xmlpull")
}
implementation("org.jsoup:jsoup:1.18.1")
// Fb2 parser

View file

@ -37,11 +37,12 @@
-dontwarn org.xmlpull.v1.XmlPullParserFactory
# Fixes issue with displaying .xml files, but may produce reproducibility issues
-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; }
#-keep class org.xmlpull.** { *; }
#-keepclassmembers class org.xmlpull.** { *; }
# Keeping names of classes that produce undeterministic results
-keepnames class **
-keepnames class org.xmlpull.** { *; }
-keepclassmembernames class org.xmlpull.** { *; }
-keepnames class kotlin.reflect.jvm.internal.impl.builtins.PrimitiveType { values(); }
-keepnames class kotlin.reflect.jvm.internal.impl.builtins.PrimitiveType { values(); }
-keepnames class * implements android.os.Parcelable { ** CREATOR; }

View file

@ -17,26 +17,6 @@
android:name="ua.acclorite.book_story.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
tools:node="remove" />
<queries>
<!-- Translator -->
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<!-- Browser -->
<intent>
<action android:name="android.intent.action.WEB_SEARCH" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
</intent>
</queries>
<application
android:name=".Application"
android:allowBackup="false"

View file

@ -1,16 +1,17 @@
package ua.acclorite.book_story.data.parser.epub
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipFile
import javax.inject.Inject
class EpubFileParser @Inject constructor() : FileParser {
@ -21,45 +22,81 @@ class EpubFileParser @Inject constructor() : FileParser {
}
try {
val epubReader = EpubReader()
val book = withContext(Dispatchers.IO) {
FileInputStream(file).use {
epubReader.readEpub(it)
var book: Pair<Book, CoverImage?>? = null
withContext(Dispatchers.IO) {
ZipFile(file).use { zip ->
val opfEntry = zip.entries().asSequence().find { entry ->
entry.name.endsWith(".opf")
} ?: return@withContext
val opfContent = zip
.getInputStream(opfEntry)
.bufferedReader()
.use { it.readText() }
val document = Jsoup.parse(opfContent)
val title = document.select("metadata > dc|title").text().trim()
val author = UIText.StringValue(
document.select("metadata > dc|creator").text().trim()
)
val description = Jsoup.parse(
document.select("metadata > dc|description").text()
).text()
var coverImagePath: String? = null
val coverId = document
.select("metadata > meta[name=cover]")
.attr("content")
if (coverId.isNotBlank()) {
coverImagePath = document
.select("manifest > item[id=$coverId]")
.attr("href")
}
if (coverImagePath.isNullOrBlank()) {
coverImagePath = document
.select("manifest > item[media-type*=image]")
.firstOrNull()?.attr("href")
}
book = Book(
title = title,
author = author,
description = description,
textPath = "",
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = null
) to extractCoverImageBitmap(file, coverImagePath)
}
}
val metadata = book.metadata
val author = UIText.StringValue(metadata.authors.joinToString(", "))
val title = metadata.titles.joinToString(", ")
val coverImage = book.coverImage?.let { BitmapFactory.decodeStream(it.inputStream) }
var description: StringBuilder? = StringBuilder()
metadata.descriptions.forEach {
description?.append(it)?.append(" ")
}
if (description != null) {
if (description.isBlank()) {
description = null
}
}
return Book(
title = title,
author = author,
description = description?.toString(),
textPath = "",
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = null
) to coverImage
return book
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
private fun extractCoverImageBitmap(file: File, coverImagePath: String?): Bitmap? {
if (coverImagePath.isNullOrBlank()) {
return null
}
ZipFile(file).use { zip ->
zip.entries().asSequence().forEach { entry ->
if (entry.name.endsWith(coverImagePath)) {
val imageBytes = zip.getInputStream(entry).readBytes()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
}
}
return null
}

View file

@ -2,19 +2,13 @@ package ua.acclorite.book_story.data.parser.epub
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import org.jsoup.nodes.Document.OutputSettings
import org.jsoup.safety.Safelist
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
import java.io.InputStreamReader
import java.nio.charset.Charset
import java.util.zip.ZipFile
import javax.inject.Inject
@ -26,66 +20,42 @@ class EpubTextParser @Inject constructor() : TextParser {
}
try {
val book = withContext(Dispatchers.IO) {
EpubReader().readEpub(FileInputStream(file))
}
val unformattedText = StringBuilder()
val lines = mutableListOf<String>()
for (spineReference in book.spine.spineReferences) {
val resource = spineReference.resource
val inputStream = resource.inputStream
val reader = BufferedReader(
InputStreamReader(
inputStream,
Charset.forName("UTF-8")
)
)
var line: String?
withContext(Dispatchers.IO) {
ZipFile(file).use { zip ->
zip.entries().asSequence().forEach { entry ->
if (
entry.name.endsWith(".xhtml")
|| entry.name.endsWith(".html")
|| entry.name.endsWith(".xml")
|| entry.name.endsWith(".htm")
) {
val content = zip.getInputStream(entry).bufferedReader()
.use {
it.readText()
}
withContext(Dispatchers.IO) {
inputStream.close()
}
while (withContext(Dispatchers.IO) { reader.readLine() }
.also { line = it } != null) {
unformattedText.append(line).append("\n")
}
withContext(Dispatchers.IO) {
reader.close()
}
}
val strings = mutableListOf<String>()
val parsedText = Jsoup.parse(unformattedText.toString())
parsedText.outputSettings(OutputSettings().prettyPrint(false))
parsedText.select("br").append("\n")
parsedText.select("p").prepend("\n")
parsedText.select("em").append(" ").prepend("")
val formattedText = Jsoup.clean(
parsedText.html(),
"",
Safelist.none(),
OutputSettings().prettyPrint(false)
)
formattedText
.replace("&nbsp;", "")
.replace("\u00a0", "")
.split("\n")
.forEach {
if (it.isNotBlank()) {
strings.add(it.trim())
val document = Jsoup.parse(content)
document.select("p").append("\n")
document
.wholeText()
.lines()
.forEach { line ->
if (line.isNotBlank()) {
lines.add(line.trim())
}
}
}
}
}
}
if (strings.isEmpty()) {
if (lines.isEmpty()) {
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
}
return Resource.Success(strings)
return Resource.Success(lines)
} catch (e: Exception) {
e.printStackTrace()
return Resource.Error(

View file

@ -58,20 +58,6 @@ class Fb2TextParser @Inject constructor() : TextParser {
return@forEachIndexed
}
if (
line.all {
if (it == ' ') {
true
} else if (it.isUpperCase() || it.isDigit() || it == '-') {
true
} else {
false
}
}
) {
return@forEachIndexed
}
if (line.all { it.isDigit() }) {
return@forEachIndexed
}

View file

@ -1,15 +1,11 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ReaderLine
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.ID
import javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(textPath: String): Map<ID, ReaderLine> {
return repository.getBookText(textPath = textPath).withIndex().associate {
it.index to ReaderLine(line = it.value)
}
suspend fun execute(textPath: String): List<String> {
return repository.getBookText(textPath = textPath)
}
}

View file

@ -20,12 +20,14 @@ import ua.acclorite.book_story.R
/**
* Custom Checkbox. Has a Circle shape.
*
* @param modifier Modifier.
* @param selected Whether checked or not.
* @param containerColor Container color.
* @param size Size of the checkbox icon(not the whole checkbox).
*/
@Composable
fun CustomCheckbox(
modifier: Modifier = Modifier,
selected: Boolean,
containerColor: Color,
size: Dp = 22.dp
@ -33,7 +35,7 @@ fun CustomCheckbox(
Icon(
imageVector = Icons.Default.Check,
tint = if (selected) containerColor else Color.Transparent,
modifier = Modifier
modifier = modifier
.border(
width = if (!selected) 1.dp else 0.dp,
if (!selected) MaterialTheme.colorScheme.outline else Color.Transparent,

View file

@ -29,8 +29,10 @@ import ua.acclorite.book_story.R
private const val MENU_ITEM_COPY = 0
private const val MENU_ITEM_TRANSLATE = 1
private const val MENU_ITEM_DICTIONARY = 2
private const val MENU_ITEM_SHARE = 1
private const val MENU_ITEM_WEB = 2
private const val MENU_ITEM_TRANSLATE = 3
private const val MENU_ITEM_DICTIONARY = 4
/**
* Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info.
@ -39,6 +41,8 @@ private class CustomTextActionModeCallback(
private val context: Context,
var rect: Rect = Rect.Zero,
var onCopyRequested: (() -> Unit)? = null,
var onShareRequested: (() -> Unit)? = null,
var onWebSearchRequested: (() -> Unit)? = null,
var onTranslateRequested: (() -> Unit)? = null,
var onDictionaryRequested: (() -> Unit)? = null
) : ActionMode.Callback {
@ -51,13 +55,23 @@ private class CustomTextActionModeCallback(
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onShareRequested?.let {
menu.add(0, MENU_ITEM_SHARE, 1, context.getString(R.string.share))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onWebSearchRequested?.let {
menu.add(0, MENU_ITEM_WEB, 2, context.getString(R.string.web_search))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onTranslateRequested?.let {
menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate))
menu.add(0, MENU_ITEM_TRANSLATE, 3, context.getString(R.string.translate))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onDictionaryRequested?.let {
menu.add(0, MENU_ITEM_DICTIONARY, 2, context.getString(R.string.dictionary))
menu.add(0, MENU_ITEM_DICTIONARY, 4, context.getString(R.string.dictionary))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
@ -71,6 +85,8 @@ private class CustomTextActionModeCallback(
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item!!.itemId) {
MENU_ITEM_COPY -> onCopyRequested?.invoke()
MENU_ITEM_SHARE -> onShareRequested?.invoke()
MENU_ITEM_WEB -> onWebSearchRequested?.invoke()
MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke()
MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke()
else -> return false
@ -116,12 +132,15 @@ private class FloatingTextActionModeCallback(
}
/**
* Custom Selection Toolbar. Used in pair with [CustomSelectionContainer] to display custom toolbar.
* Custom Selection Toolbar.
* Used in pair with [CustomSelectionContainer] to display custom toolbar.
*/
private class CustomSelectionToolbar(
private val view: View,
context: Context,
private val onCopyRequest: (() -> Unit)?,
private val onShareRequest: ((String) -> Unit)?,
private val onWebSearchRequest: ((String) -> Unit)?,
private val onTranslateRequest: ((String) -> Unit)?,
private val onDictionaryRequest: ((String) -> Unit)?
) : TextToolbar {
@ -145,6 +164,36 @@ private class CustomSelectionToolbar(
onCopyRequested?.invoke()
onCopyRequest?.invoke()
}
callback.onShareRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
val currentClipboard = clipboardManager.text
onShareRequest?.invoke(currentClipboard.toString())
if (previousClipboard != null) {
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onWebSearchRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
val currentClipboard = clipboardManager.text
onWebSearchRequest?.invoke(currentClipboard.toString())
if (previousClipboard != null) {
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onTranslateRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
@ -205,6 +254,8 @@ private class CustomSelectionToolbar(
@Composable
fun CustomSelectionContainer(
onCopyRequested: (() -> Unit),
onShareRequested: ((String) -> Unit),
onWebSearchRequested: ((String) -> Unit),
onTranslateRequested: ((String) -> Unit),
onDictionaryRequested: ((String) -> Unit),
content: @Composable (toolbarHidden: Boolean) -> Unit
@ -220,6 +271,12 @@ fun CustomSelectionContainer(
onCopyRequest = {
onCopyRequested()
},
onShareRequest = {
onShareRequested(it)
},
onWebSearchRequest = {
onWebSearchRequested(it)
},
onTranslateRequest = {
onTranslateRequested(it)
},

View file

@ -60,7 +60,7 @@ fun CustomDialogWithContent(
modifier: Modifier = Modifier,
drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f,
backgroundTransparency: Float = 0.5f,
title: String,
description: String?,
actionText: String?,

View file

@ -62,7 +62,7 @@ fun CustomDialogWithLazyColumn(
modifier: Modifier = Modifier,
drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f,
backgroundTransparency: Float = 0.5f,
title: String,
description: String?,
actionText: String?,

View file

@ -15,16 +15,18 @@ fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",",
fun Intent.launchActivity(
activity: ComponentActivity,
createChooser: Boolean = false,
openInNewWindow: Boolean = true,
success: (() -> Unit)? = null,
error: () -> Unit
) {
val intent = if (createChooser) Intent.createChooser(this, "") else this
if (openInNewWindow) {
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
activity.baseContext.startActivity(this)
activity.baseContext.startActivity(intent)
} catch (e: Exception) {
error()
return

View file

@ -90,7 +90,9 @@ fun LicenseItem(
Text(
text = library.author,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
if (library.licenses.isNotEmpty()) {

View file

@ -534,7 +534,7 @@ class BookInfoViewModel @Inject constructor(
val updatedText = updatedBook.text
val text = getText.execute(book.textPath)
if (updatedText != text.map { it.value.line }) {
if (updatedText != text) {
textUpdated = true
}

View file

@ -64,7 +64,7 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
Spacer(modifier = Modifier.width(24.dp))
CustomCheckbox(
selected = result.second,
MaterialTheme.colorScheme.surfaceContainerHigh
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
)
}
}

View file

@ -52,7 +52,7 @@ import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.customItems
import ua.acclorite.book_story.presentation.components.customItemsIndexed
import ua.acclorite.book_story.presentation.components.is_messages.IsError
import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent
@ -188,9 +188,6 @@ private fun ReaderScreen(
}
}
val text = remember(state.value.text) {
state.value.text.toList().sortedBy { it.first }
}
val fontFamily = remember(mainState.value.fontFamily) {
Constants.FONTS.find {
it.id == mainState.value.fontFamily
@ -277,10 +274,41 @@ private fun ReaderScreen(
).show()
}
},
onShareRequested = { textToShare ->
onEvent(
ReaderEvent.OnOpenShareApp(
textToShare = textToShare,
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_share_app),
Toast.LENGTH_SHORT
).show()
}
)
)
},
onWebSearchRequested = { textToSearch ->
onEvent(
ReaderEvent.OnOpenWebBrowser(
textToSearch = textToSearch,
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
},
onTranslateRequested = { textToTranslate ->
onEvent(
ReaderEvent.OnOpenTranslator(
textToTranslate,
textToTranslate = textToTranslate,
translateWholeParagraph = false,
context,
noAppsFound = {
Toast.makeText(
@ -300,7 +328,7 @@ private fun ReaderScreen(
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
context.getString(R.string.error_no_dictionary),
Toast.LENGTH_SHORT
).show()
}
@ -359,11 +387,11 @@ private fun ReaderScreen(
Spacer(modifier = Modifier.height(18.dp))
}
customItems(
text, key = { key -> key.first }
) { line ->
customItemsIndexed(
state.value.text, key = { _, index -> index }
) { _, line ->
ReaderTextParagraph(
line = line.second,
line = line,
context = context,
fontFamily = fontFamily,
fontColor = fontColor.value,

View file

@ -22,7 +22,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.FontWithName
import ua.acclorite.book_story.domain.model.ReaderLine
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
/**
@ -44,7 +43,7 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.ReaderTextParagraph(
line: ReaderLine,
line: String,
context: Context,
fontFamily: FontWithName,
fontColor: Color,
@ -72,7 +71,7 @@ fun LazyItemScope.ReaderTextParagraph(
append(" ")
}
append(line.line)
append(line)
},
modifier = Modifier.then(
if (
@ -85,7 +84,8 @@ fun LazyItemScope.ReaderTextParagraph(
onDoubleClick = {
onEvent(
ReaderEvent.OnOpenTranslator(
textToTranslate = line.line,
textToTranslate = line,
translateWholeParagraph = true,
context = context as ComponentActivity,
noAppsFound = {
Toast.makeText(

View file

@ -46,6 +46,19 @@ sealed class ReaderEvent {
data class OnOpenTranslator(
val textToTranslate: String,
val translateWholeParagraph: Boolean,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnOpenShareApp(
val textToShare: String,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnOpenWebBrowser(
val textToSearch: String,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()

View file

@ -3,15 +3,13 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.ReaderLine
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.ID
import ua.acclorite.book_story.domain.util.UIText
@Immutable
data class ReaderState(
val book: Book = Constants.EMPTY_BOOK,
val text: Map<ID, ReaderLine> = emptyMap(),
val text: List<String> = emptyList(),
val words: Int = 0,
val letters: Int = 0,
val listState: LazyListState = LazyListState(),

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import android.app.SearchManager
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.compose.runtime.snapshotFlow
@ -73,7 +74,6 @@ class ReaderViewModel @Inject constructor(
}
val textAsLine = text
.map { it.value.line }
.joinToString(
separator = "\n"
)
@ -338,7 +338,10 @@ class ReaderViewModel @Inject constructor(
)
var translatorFailure = false
translatorIntent.launchActivity(event.context) {
translatorIntent.launchActivity(
event.context,
createChooser = !event.translateWholeParagraph
) {
translatorFailure = true
}
if (!translatorFailure) {
@ -357,22 +360,85 @@ class ReaderViewModel @Inject constructor(
}
}
is ReaderEvent.OnOpenDictionary -> {
is ReaderEvent.OnOpenShareApp -> {
launch {
val shareIntent = Intent()
shareIntent.action = Intent.ACTION_SEND
shareIntent.type = "text/plain"
shareIntent.putExtra(
Intent.EXTRA_SUBJECT,
event.context.getString(R.string.app_name)
)
shareIntent.putExtra(
Intent.EXTRA_TEXT,
event.textToShare.trim()
)
var shareFailure = false
shareIntent.launchActivity(event.context, createChooser = true) {
shareFailure = true
}
if (!shareFailure) {
return@launch
}
event.noAppsFound()
}
}
is ReaderEvent.OnOpenWebBrowser -> {
launch {
val browserIntent = Intent()
browserIntent.action = Intent.ACTION_WEB_SEARCH
browserIntent.putExtra(
SearchManager.QUERY,
"dictionary" +
": ${event.textToDefine.trim()}"
event.textToSearch
)
var failure = false
var browserFailure = false
browserIntent.launchActivity(event.context) {
failure = true
browserFailure = true
}
if (!failure) {
if (!browserFailure) {
return@launch
}
event.noAppsFound()
}
}
is ReaderEvent.OnOpenDictionary -> {
launch {
val dictionaryIntent = Intent()
val browserIntent = Intent()
dictionaryIntent.type = "text/plain"
dictionaryIntent.action = Intent.ACTION_PROCESS_TEXT
dictionaryIntent.putExtra(
Intent.EXTRA_PROCESS_TEXT,
event.textToDefine.trim()
)
dictionaryIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
browserIntent.action = Intent.ACTION_VIEW
val text = event.textToDefine.trim().replace(" ", "+")
browserIntent.data = Uri.parse("https://www.onelook.com/?w=$text")
var dictionaryFailure = false
dictionaryIntent.launchActivity(event.context, createChooser = true) {
dictionaryFailure = true
}
if (!dictionaryFailure) {
return@launch
}
var browserFailure = false
browserIntent.launchActivity(event.context) {
browserFailure = true
}
if (!browserFailure) {
return@launch
}

View file

@ -1,38 +1,70 @@
package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.SegmentedButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.Icon
import androidx.compose.material3.SegmentedButtonColors
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
/**
* Segmented Button with Title. Contains [buttons].
* Segmented Button with Title.
* Uses custom implementation(material3 one has many flaws).
* Contains [buttons].
*
* @param modifier Modifier.
* @param title Title of the buttons.
* @param buttons [ButtonItem]s.
* @param enabled Whether button is enabled.
* @param horizontalPadding Horizontal item padding.
* @param verticalPadding Vertical item padding.
* @param onClick OnClick callback.
*/
@Composable
fun SegmentedButtonWithTitle(
modifier: Modifier = Modifier,
title: String,
buttons: List<ButtonItem>,
locked: Boolean,
enabled: Boolean = true,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onClick: (ButtonItem) -> Unit
) {
val colors = SegmentedButtonDefaults.colors()
Column(
modifier
.fillMaxWidth()
@ -42,50 +74,118 @@ fun SegmentedButtonWithTitle(
Spacer(modifier = Modifier.height(8.dp))
SingleChoiceSegmentedButtonRow {
buttons.forEachIndexed { index, buttonItem ->
SegmentedButton(
enabled = locked,
selected = buttonItem.selected,
onClick = { onClick(buttonItem) },
shape = when (index) {
buttons.lastIndex -> RoundedCornerShape(
topEndPercent = 100,
bottomEndPercent = 100
LazyRow(Modifier.fillMaxWidth()) {
item {
Row(
Modifier
.clip(CircleShape)
.border(
width = 0.5.dp,
color = SegmentedButtonDefaults.colors().activeBorderColor,
shape = CircleShape
)
.padding(0.5.dp)
) {
buttons.forEachIndexed { index, buttonItem ->
SegmentedButton(
button = buttonItem,
enabled = enabled,
shape = when (index) {
buttons.lastIndex -> RoundedCornerShape(
topEndPercent = 100,
bottomEndPercent = 100
)
0 -> RoundedCornerShape(
topStartPercent = 100,
bottomStartPercent = 100
)
0 -> RoundedCornerShape(
topStartPercent = 100,
bottomStartPercent = 100
)
else -> RoundedCornerShape(0)
},
colors = SegmentedButtonDefaults.colors(
disabledInactiveBorderColor = colors.activeBorderColor,
disabledInactiveContentColor = colors.inactiveContentColor,
disabledActiveContainerColor = colors.activeContainerColor,
disabledActiveContentColor = colors.activeContentColor,
disabledInactiveContainerColor = colors.inactiveContainerColor,
disabledActiveBorderColor = colors.activeBorderColor,
),
label = {
Text(
text = buttonItem.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
else -> RoundedCornerShape(0)
},
onClick = { onClick(buttonItem) }
)
}
)
}
}
}
}
}
/**
* Custom Segmented button.
* Adjusts width based on component name.
*
* @param button [ButtonItem].
* @param enabled Whether can be clicked.
* @param shape Shape of the button. For proper implementation should have 100% corners on first and last buttons(start and end).
* @param colors [SegmentedButtonColors].
* @param onClick OnClick callback.
*/
@Composable
private fun SegmentedButton(
button: ButtonItem,
enabled: Boolean,
shape: RoundedCornerShape,
colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
onClick: () -> Unit
) {
Row(
modifier = Modifier
.height(40.dp)
.clip(shape)
.clickable(enabled = enabled && !button.selected) {
onClick()
}
.border(
width = 0.5.dp,
color = colors.activeBorderColor,
shape = shape
)
.padding(0.5.dp)
.background(
if (button.selected) colors.activeContainerColor
else Color.Transparent,
shape = shape
)
.padding(horizontal = 18.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAnimatedVisibility(
visible = button.selected,
enter = expandHorizontally()
+ slideInVertically(initialOffsetY = { it / 2 })
+ scaleIn()
+ fadeIn(),
exit = shrinkHorizontally()
+ slideOutVertically(targetOffsetY = { it / 2 })
+ scaleOut()
+ fadeOut()
) {
Row {
Icon(
imageVector = Icons.Default.Done,
contentDescription = stringResource(id = R.string.selected_content_desc),
modifier = Modifier
.size(18.dp),
tint = colors.activeContentColor
)
Spacer(modifier = Modifier.width(8.dp))
}
}
Text(
text = button.title,
style = button.textStyle,
fontWeight = FontWeight.Medium,
color = if (button.selected) colors.activeContentColor
else colors.inactiveContentColor
)
}
}

View file

@ -25,17 +25,16 @@ fun LazyItemScope.DarkThemeSetting(
SegmentedButtonWithTitle(
title = stringResource(id = R.string.dark_theme_option),
modifier = Modifier.animateItem(),
locked = true,
buttons = DarkTheme.entries.map {
ButtonItem(
it.toString(),
when (it) {
title = when (it) {
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
},
MaterialTheme.typography.labelLarge,
it == state.value.darkTheme
textStyle = MaterialTheme.typography.labelLarge,
selected = it == state.value.darkTheme
)
}
) {

View file

@ -33,17 +33,16 @@ fun LazyItemScope.PureDarkSetting(
) {
SegmentedButtonWithTitle(
title = stringResource(id = R.string.pure_dark_option),
locked = true,
buttons = PureDark.entries.map {
ButtonItem(
it.toString(),
when (it) {
id = it.toString(),
title = when (it) {
PureDark.OFF -> stringResource(id = R.string.pure_dark_off)
PureDark.ON -> stringResource(id = R.string.pure_dark_on)
PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
},
MaterialTheme.typography.labelLarge,
it == state.value.pureDark
textStyle = MaterialTheme.typography.labelLarge,
selected = it == state.value.pureDark
)
}
) {

View file

@ -54,17 +54,17 @@ fun LazyItemScope.ThemeContrastSetting(
) {
SegmentedButtonWithTitle(
title = stringResource(id = R.string.theme_contrast_option),
locked = state.value.theme != Theme.DYNAMIC,
enabled = state.value.theme != Theme.DYNAMIC,
buttons = ThemeContrast.entries.map {
ButtonItem(
it.toString(),
when (it) {
id = it.toString(),
title = when (it) {
ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard)
ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium)
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
},
MaterialTheme.typography.labelLarge,
it == state.value.themeContrast
textStyle = MaterialTheme.typography.labelLarge,
selected = it == state.value.themeContrast
)
}
) {

View file

@ -35,12 +35,12 @@ fun LazyItemScope.FontFamilySetting(
chips = Constants.FONTS
.map {
ButtonItem(
it.id,
it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy(
id = it.id,
title = it.fontName.asString(),
textStyle = MaterialTheme.typography.labelLarge.copy(
fontFamily = it.font
),
it.id == fontFamily.id
selected = it.id == fontFamily.id
)
},
onClick = {

View file

@ -13,7 +13,7 @@ import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle
/**
* Font Style setting.
@ -30,27 +30,27 @@ fun LazyItemScope.FontStyleSetting(
} ?: Constants.FONTS[0]
}
ChipsWithTitle(
SegmentedButtonWithTitle(
title = stringResource(id = R.string.font_style_option),
modifier = Modifier.animateItem(),
chips = listOf(
buttons = listOf(
ButtonItem(
"normal",
stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy(
id = "normal",
title = stringResource(id = R.string.font_style_normal),
textStyle = MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal
),
!state.value.isItalic!!
selected = !state.value.isItalic!!
),
ButtonItem(
"italic",
stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy(
id = "italic",
title = stringResource(id = R.string.font_style_italic),
textStyle = MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic
),
state.value.isItalic!!
selected = state.value.isItalic!!
),
),
onClick = {

View file

@ -64,10 +64,10 @@ fun StartSettings(
val languages = remember(mainState.value.language) {
Constants.LANGUAGES.sortedBy { it.second }.map {
ButtonItem(
it.first,
it.second,
TextStyle(),
it.first == mainState.value.language
id = it.first,
title = it.second,
textStyle = TextStyle(),
selected = it.first == mainState.value.language
)
}.sortedBy { it.title }
}

View file

@ -58,22 +58,6 @@ fun DefaultTransition(
}
}
@Composable
fun <T> T.DefaultTransition(
visible: Boolean,
modifier: Modifier = Modifier,
content: @Composable T.() -> Unit
) {
CustomAnimatedVisibility(
visible = visible,
modifier = modifier,
enter = Transitions.DefaultTransitionIn,
exit = Transitions.DefaultTransitionOut
) {
content()
}
}
@Composable
fun SlidingTransition(
visible: Boolean,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m260,700 l300,-140 140,-300 -300,140 -140,300ZM480,520q-17,0 -28.5,-11.5T440,480q0,-17 11.5,-28.5T480,440q17,0 28.5,11.5T520,480q0,17 -11.5,28.5T480,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m260,700 l300,-140 140,-300 -300,140 -140,300ZM480,520q-17,0 -28.5,-11.5T440,480q0,-17 11.5,-28.5T480,440q17,0 28.5,11.5T520,480q0,17 -11.5,28.5T480,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M226,788q-21.75,0 -37.87,-16.13Q172,755.75 172,734v-508q0,-21.75 16.13,-37.88Q204.25,172 226,172h205q-13,-27.5 2.95,-53.75 15.94,-26.25 46,-26.25Q511,92 527,118.25q16,26.25 2,53.75h205q21.75,0 37.88,16.12Q788,204.25 788,226v508q0,21.75 -16.12,37.87Q755.75,788 734,788L226,788ZM226,766h508q12,0 22,-10t10,-22v-508q0,-12 -10,-22t-22,-10L226,194q-12,0 -22,10t-10,22v508q0,12 10,22t22,10ZM306,642h226v-22L306,620v22ZM306,491h348v-22L306,469v22ZM306,340h348v-22L306,318v22ZM479.79,178q12.79,0 22.5,-9t9.71,-22.5q0,-13.5 -9.71,-22.5t-22.5,-9Q467,115 458,124t-9,22.5q0,13.5 9,22.5t21.79,9ZM194,766v-572,572Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M479.99,839q-49.99,0 -84.49,-34.51Q361,769.98 361,720q0,-47 31,-80.5t77,-38.5v-110L255,491v-136h-98v-218h218v218h-98v114h406v-114q-46,-5 -77,-38.29 -31,-33.3 -31,-80.71 0,-49.98 34.51,-84.49Q644.03,117 694.01,117q49.99,0 84.49,34.51Q813,186.02 813,236q0,47 -31,80.5T705,355v136L491,491v110q46,5 77,38.29 31,33.3 31,80.71 0,49.98 -34.51,84.49Q529.97,839 479.99,839ZM693.87,333q40.13,0 68.63,-28.37 28.5,-28.37 28.5,-68.5 0,-40.13 -28.37,-68.63 -28.37,-28.5 -68.5,-28.5 -40.13,0 -68.63,28.37 -28.5,28.37 -28.5,68.5 0,40.13 28.37,68.63 28.37,28.5 68.5,28.5ZM179,333h174v-174L179,159v174ZM479.87,817q40.13,0 68.63,-28.37 28.5,-28.37 28.5,-68.5 0,-40.13 -28.37,-68.63 -28.37,-28.5 -68.5,-28.5 -40.13,0 -68.63,28.37 -28.5,28.37 -28.5,68.5 0,40.13 28.37,68.63 28.37,28.5 68.5,28.5ZM266,246ZM694,236ZM480,720Z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M278,566v22h-92q-21.75,0 -37.87,-16.13Q132,555.75 132,534v-348q0,-21.75 16.13,-37.88Q164.25,132 186,132h348q21.75,0 37.88,16.12Q588,164.25 588,186v92h-22v-92q0,-14 -9,-23t-23,-9L186,154q-14,0 -23,9t-9,23v348q0,14 9,23t23,9h92ZM426,828q-21.75,0 -37.87,-16.13Q372,795.75 372,774v-348q0,-21.75 16.13,-37.88Q404.25,372 426,372h348q21.75,0 37.88,16.12Q828,404.25 828,426v348q0,21.75 -16.12,37.87Q795.75,828 774,828L426,828ZM426,806h348q14,0 23,-9t9,-23v-348q0,-14 -9,-23t-23,-9L426,394q-14,0 -23,9t-9,23v348q0,14 9,23t23,9ZM600,600Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M480,680q14,0 23.5,-9.5T513,647q0,-14 -9.5,-23.5T480,614q-14,0 -23.5,9.5T447,647q0,14 9.5,23.5T480,680ZM453,527h60v-253h-60v253ZM481,931L346,800L160,800v-186L26,480l134,-134v-186h186l135,-134 133,134h186v186l134,134 -134,134v186L614,800L481,931Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:fillType="evenOdd"
android:pathData="M512,0C229.1,0 0,229.1 0,512C0,738.6 146.6,929.9 350.1,997.8C375.7,1002.2 385.3,986.9 385.3,973.4C385.3,961.3 384.6,921 384.6,878.1C256,901.8 222.7,846.7 212.5,817.9C206.7,803.2 181.8,757.8 160,745.6C142.1,736 116.5,712.3 159.4,711.7C199.7,711 228.5,748.8 238.1,764.2C284.2,841.6 357.8,819.8 387.2,806.4C391.7,773.1 405.1,750.7 419.8,737.9C305.9,725.1 186.9,681 186.9,485.1C186.9,429.4 206.7,383.4 239.4,347.5C234.2,334.7 216.3,282.2 244.5,211.8C244.5,211.8 287.4,198.4 385.3,264.3C426.2,252.8 469.8,247 513.3,247C556.8,247 600.3,252.8 641.3,264.3C739.2,197.8 782.1,211.8 782.1,211.8C810.2,282.2 792.3,334.7 787.2,347.5C819.8,383.4 839.7,428.8 839.7,485.1C839.7,681.6 720,725.1 606.1,737.9C624.6,753.9 640.6,784.6 640.6,832.6C640.6,901.1 640,956.2 640,973.4C640,986.9 649.6,1002.9 675.2,997.8C877.4,929.9 1024,737.9 1024,512C1024,229.1 794.9,0 512,0Z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m612,668 l56,-56 -148,-148v-184h-80v216l172,172ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m612,668 l56,-56 -148,-148v-184h-80v216l172,172ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,480ZM480,800q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160q-133,0 -226.5,93.5T160,480q0,133 93.5,226.5T480,800Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="M160,880q-33,0 -56.5,-23.5T80,800v-560h80v560h560v80L160,880ZM320,720q-33,0 -56.5,-23.5T240,640v-480q0,-33 23.5,-56.5T320,80h480q33,0 56.5,23.5T880,160v480q0,33 -23.5,56.5T800,720L320,720ZM520,440 L620,380 720,440v-280L520,160v280Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="M320,640h480v-480h-80v280l-100,-60 -100,60v-280L320,160v480ZM320,720q-33,0 -56.5,-23.5T240,640v-480q0,-33 23.5,-56.5T320,80h480q33,0 56.5,23.5T880,160v480q0,33 -23.5,56.5T800,720L320,720ZM160,880q-33,0 -56.5,-23.5T80,800v-560h80v560h560v80L160,880ZM520,160h200,-200ZM320,160h480,-480Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1,45 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="256"
android:viewportWidth="256"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M111.07,140.52c0,-6.98 -5.69,-12.67 -12.67,-12.67c-6.98,0 -12.67,5.69 -12.67,12.67c0,6.98 5.66,12.6 12.67,12.67C105.38,153.19 111.07,147.5 111.07,140.52z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81" />
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M154.26,170.73c-7.7,5.5 -17,8.2 -26.43,7.7c-9.43,0.44 -18.7,-2.39 -26.31,-7.95c-1.29,-1.04 -3.08,-1.04 -4.34,0c-1.45,1.19 -1.67,3.36 -0.47,4.81c8.99,6.76 19.99,10.22 31.24,9.75c11.25,0.47 22.26,-2.99 31.24,-9.75l0,0.5c1.38,-1.35 1.41,-3.61 0.06,-5C157.9,169.41 155.64,169.38 154.26,170.73z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81" />
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M156.93,128.29c-6.98,0 -12.67,5.69 -12.67,12.67c0,6.98 5.69,12.67 12.67,12.67l-0.1,0.47c0.22,0 0.41,0 0.63,0c6.98,-0.28 12.41,-6.16 12.13,-13.14C169.6,133.99 163.91,128.29 156.93,128.29z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81" />
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M127.86,1.41C58.02,1.41 1.41,58.02 1.41,127.86c0,69.84 56.61,126.45 126.45,126.45c69.84,0 126.45,-56.61 126.45,-126.45C254.31,58.02 197.69,1.41 127.86,1.41zM201.97,144.67c0.13,1.85 0.13,3.71 0,5.56c0,28.35 -33,51.36 -73.71,51.36s-73.71,-23.04 -73.71,-51.36c-0.13,-1.85 -0.13,-3.71 0,-5.56c-1.85,-0.82 -3.52,-1.98 -4.97,-3.33c-7.45,-6.98 -7.79,-18.67 -0.82,-26.09c6.98,-7.45 18.67,-7.79 26.09,-0.82c14.58,-9.87 31.71,-15.28 49.32,-15.56l9.33,-43.88c0,-0.03 0,-0.03 0,-0.06c0.47,-2.14 2.58,-3.46 4.68,-2.99l30.99,6.19c1.98,-3.46 5.56,-5.79 9.55,-6.19c6.95,-0.76 13.17,4.27 13.92,11.22s-4.27,13.17 -11.22,13.92c-6.95,0.76 -13.17,-4.27 -13.92,-11.22l-27.06,-5.69l-8.2,39.45c17.38,0.38 34.29,5.75 48.69,15.56c3.27,-3.11 7.57,-4.97 12.1,-5.12c10.21,-0.35 18.77,7.61 19.14,17.79C212.31,134.96 208.32,141.5 201.97,144.67z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M184.69,780v-30.77h81.69v-234L84.62,240h395.07L297.15,515.23v234h82.47L379.62,780L184.69,780ZM194.62,353.69h174.3l54.23,-82.92h-282l53.47,82.92ZM613.55,780q-42.4,0 -70.28,-27.79 -27.89,-27.8 -27.89,-70.2 0,-41.63 27.83,-69.51 27.82,-27.88 70.25,-27.88 16.84,0 33.27,6.07 16.42,6.08 33.27,18.77L680,240h155.38v61.31L710.77,301.31v380.61q0,42.44 -27.79,70.26Q655.18,780 613.55,780Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="39.867767dp"
android:viewportHeight="402"
android:viewportWidth="242"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m121,1c-9.32,10.58 -15,24.42 -15,39.62 0.55,33.37 4.64,66.7 5,100 0.74,31.1 -8.58,60.3 -19.92,88.8 -3.78,7.87 -7.86,15.57 -11.95,23.29l-12,-2.42c-10.83,-2.16 -17.87,-12.69 -15.7,-23.52 1.89,-9.47 10.23,-16.1 19.53,-16.1l4.38,0.47l-9.77,-81.6c-3.19,-36.3 -21.93,-68.2 -49.5,-88.9 -4.74,-3.56 -9.77,-6.82 -15,-9.69v289.7h66.88c5,27.1 19.94,50.8 40.94,67 4.99,3.44 9.18,7.98 12.19,13.28 3,-5.3 7.19,-9.84 12.19,-13.28 21,-16.24 35.94,-39.89 40.94,-67h66.88v-289.7c-5.23,2.87 -10.26,6.13 -15,9.69 -27.6,20.73 -46.3,52.6 -49.5,88.9l-9.77,81.6l4.38,-0.47c9.3,0.04 17.64,6.62 19.53,16.1 2.16,10.83 -4.88,21.35 -15.7,23.52l-12,2.42c-4.09,-7.71 -8.18,-15.41 -11.95,-23.29 -11.35,-28.48 -20.67,-57.68 -19.93,-88.8 0.36,-33.4 4.45,-66.7 5,-100 0,-15.2 -5.68,-29 -15,-39.62zM21,72.7c12.94,15.19 21.63,34.1 24.2,54.93l8,67.3c-10.25,5.1 -18,14.46 -20.94,25.79h-11.33v-148zM221,72.7v148h-11.33c-2.92,-11.32 -10.69,-20.68 -20.94,-25.79l8,-67.3c2.59,-20.81 11.28,-39.74 24.2,-54.94zM121,208c5.37,17.57 12.76,34.3 21.95,49.78 -8.74,2.65 -16.34,7.81 -21.95,14.76 -5.61,-6.95 -13.21,-12.1 -21.95,-14.77 9.19,-15.51 16.58,-32.2 21.95,-49.77zM21,240.74h11.33c3.57,13.81 14.35,24.71 28,28.52l9.61,2.19c-2.57,9.32 -3.98,19.1 -3.98,29.3h-45v-60zM209.7,240.74h11.33v60h-45c0,-10.14 -1.41,-19.97 -3.98,-29.3l9.61,-2.19c13.7,-3.81 24.47,-14.72 28,-28.52zM89.5,276.04c12.13,1.71 21.48,12.1 21.48,24.69h-25c0,-8.57 1.27,-16.84 3.52,-24.69zM152.47,276.04c2.25,7.85 3.52,16.1 3.52,24.69h-25c0,-12.61 9.35,-22.98 21.48,-24.69zM88.27,320.74h22.73v42.1c-11,-11.55 -19,-26 -22.73,-42.1zM131,320.74h22.73c-3.68,16.1 -11.7,30.57 -22.73,42.1v-42.1z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="21.68dp"
android:viewportHeight="271"
android:viewportWidth="300"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m236,0h46l-101,115 118,156h-92.6l-72.5,-94.8 -83,94.8h-46l107,-123 -113,-148h94.9l65.5,86.6zM219.9,244h25.5l-165,-218h-27.4z" />
</vector>

View file

@ -101,7 +101,9 @@
<string name="error_query">Помилка: %1$s</string>
<string name="error_file_empty">Цей файл пустий або пошкоджений.</string>
<string name="error_no_translator">Не знайдено жодного перекладача.</string>
<string name="error_no_dictionary">Не знайдено жодного словника.</string>
<string name="error_no_browser">Не знайдено жодного браузера.</string>
<string name="error_no_share_app">Не знайдено застосунка щоб поділитися.</string>
<string name="error_no_text">
Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її.
</string>
@ -160,6 +162,8 @@
<string name="download">Завантажити</string>
<string name="done">Готово</string>
<string name="enable">Увімкнути</string>
<string name="web_search">Веб-пошук</string>
<string name="share">Поділитися</string>
<!-- Items -->
<string name="files">файли</string>

View file

@ -118,7 +118,9 @@
<string name="error_query">Error: %1$s</string>
<string name="error_file_empty">This file is empty or corrupted.</string>
<string name="error_no_translator">No translator app found.</string>
<string name="error_no_dictionary">No dictionary app found.</string>
<string name="error_no_browser">No browser app found.</string>
<string name="error_no_share_app">No app to share found.</string>
<string name="error_no_text">
The book content is empty. Please, make sure that the book content is not empty and update it.
</string>
@ -176,6 +178,8 @@
<string name="download">Download</string>
<string name="done">Done</string>
<string name="enable">Enable</string>
<string name="web_search">Web search</string>
<string name="share">Share</string>
<!-- Items -->
<string name="files">files</string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1,8 @@
* Added loading indicator in AboutScreen's check for updates
* Reduced APK size (30Mb → 12Mb)
* Improved performance
* Improved text parsers
* Fixed various crashes
* Various small changes
* Removed no longer valid Credits
* More small changes

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 983 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

After

Width:  |  Height:  |  Size: 947 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 985 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

After

Width:  |  Height:  |  Size: 947 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB