diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b10513d..48ee2fe 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -30,8 +30,8 @@ android {
applicationId = "com.aryan.reader"
minSdk = 26
targetSdk = 35
- versionCode = 42
- versionName = "1.0.41"
+ versionCode = 43
+ versionName = "1.0.42"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js
index fd7071a..0cdc0a2 100644
--- a/app/src/main/assets/epub_reader.js
+++ b/app/src/main/assets/epub_reader.js
@@ -481,83 +481,60 @@
var newGap = parseFloat(paragraphGap);
if (isNaN(newFontSize) || newFontSize < 0.5 || newFontSize > 5.0) newFontSize = 1.0;
- if (isNaN(newLineHeight) || newLineHeight < 1.0 || newLineHeight > 3.0) newLineHeight = 1.6;
+ if (isNaN(newLineHeight) || newLineHeight < 1.0 || newLineHeight > 3.0) newLineHeight = 1.0;
if (isNaN(newGap) || newGap < 0.0 || newGap > 3.0) newGap = 1.0;
var fontCss = "";
- var selector = "body";
-
if (fontFamily && fontFamily !== "Original" && fontFamily !== "") {
var fallback = "sans-serif";
-
if (fontFamily === "Merriweather" || fontFamily === "Lora") {
fallback = "serif";
} else if (fontFamily === "Roboto Mono") {
fallback = "monospace";
}
-
- selector = "body, p, span, div, li, a, h1, h2, h3, h4, h5, h6, blockquote, td, th";
- fontCss = "font-family: '" + fontFamily + "', " + fallback + " !important;";
+ fontCss = `
+ body, p, span, div, li, a, h1, h2, h3, h4, h5, h6, blockquote, td, th {
+ font-family: '${fontFamily}', ${fallback} !important;
+ }
+ `;
}
- // --- ALIGNMENT LOGIC ---
var alignCss = "";
var alignSelector = "body, p, li, div, h1, h2, h3, h4, h5, h6";
-
if (textAlign === "left") {
- alignCss =
- ` ` +
- alignSelector +
- ` {
- text-align: left !important;
- }
-
- `;
+ alignCss = alignSelector + " { text-align: left !important; }";
} else if (textAlign === "justify") {
- alignCss =
- ` ` +
- alignSelector +
- ` {
- text-align: justify !important;
- -webkit-hyphens: auto !important;
- hyphens: auto !important;
- }
-
- `;
+ alignCss = alignSelector + " { text-align: justify !important; -webkit-hyphens: auto !important; hyphens: auto !important; }";
}
- // --- GAP LOGIC ---
- var gapCss = `
+ var gapCss = "";
+ if (newGap !== 1.0) {
+ gapCss = `
body p, body ul, body ol, body blockquote {
- margin-top: ` + (0.5 * newGap) + `em !important;
- margin-bottom: ` + (0.5 * newGap) + `em !important;
+ margin-top: ${newGap}em !important;
+ margin-bottom: ${newGap}em !important;
}
body li {
- margin-bottom: ` + (0.25 * newGap) + `em !important;
+ margin-bottom: ${0.5 * newGap}em !important;
}
- `;
+ `;
+ }
- dynamicStyleElement.innerHTML =
- ` body {
- font-size: ` +
- newFontSize +
- `em !important;
- line-height: ` +
- newLineHeight +
- ` !important;
+ var sizeCss = "";
+ if (newFontSize !== 1.0) {
+ sizeCss = `body { font-size: ${newFontSize}em !important; }`;
+ }
+
+ var lineHeightCss = "";
+ if (newLineHeight !== 1.0) {
+ lineHeightCss = `
+ body, p, div, span, li, a, h1, h2, h3, h4, h5, h6, blockquote, td, th {
+ line-height: ${newLineHeight} !important;
}
+ `;
+ }
- ` +
- selector +
- ` {
- ` +
- fontCss +
- `
- }
-
- ` +
- alignCss +
- gapCss;
+ dynamicStyleElement.innerHTML = [sizeCss, lineHeightCss, fontCss, alignCss, gapCss].join("\n");
setTimeout(
function () {
@@ -565,29 +542,20 @@
console.log(logTag + ": [BODY] Computed font-family: " + computedBody);
var firstPara = document.querySelector("p");
-
if (firstPara) {
var computedPara = window.getComputedStyle(firstPara).fontFamily;
- var computedAlign = window.getComputedStyle(firstPara).textAlign;
- console.log(logTag + ": [PARAGRAPH] Computed font-family: " + computedPara);
- console.log(logTag + ": [PARAGRAPH] Inner Text Sample: " + firstPara.innerText.substring(0, 20));
- } else {
- console.log(logTag + ": [PARAGRAPH] No
tag found to check.");
+ console.log(logTag + ":[PARAGRAPH] Computed font-family: " + computedPara);
}
if (fontFamily && fontFamily !== "") {
var isCheckAvailable = document.fonts && document.fonts.check;
-
if (isCheckAvailable) {
var loaded = document.fonts.check("12px '" + fontFamily + "'");
console.log(logTag + ": Font Loading Status -> document.fonts.check('" + fontFamily + "') = " + loaded);
- } else {
- console.log(logTag + ": document.fonts API not available.");
}
}
},
-
- 300,
+ 300
);
if (window.triggerInitialScrollStateReport) {
diff --git a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
index d656f07..b1b304e 100644
--- a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
+++ b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
@@ -396,6 +396,8 @@ class FolderSyncWorker(
return when {
mimeType == "application/pdf" || lowerName.endsWith(".pdf") -> FileType.PDF
mimeType == "application/epub+zip" || lowerName.endsWith(".epub") -> FileType.EPUB
+ mimeType == "application/vnd.oasis.opendocument.text" || lowerName.endsWith(".odt") -> FileType.ODT
+ mimeType == "application/x-vnd.oasis.opendocument.text-flat-xml" || lowerName.endsWith(".fodt") -> FileType.FODT
mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || lowerName.endsWith(".docx") -> FileType.DOCX
lowerName.endsWith(".mobi") || lowerName.endsWith(".azw3") || lowerName.endsWith(".prc") -> FileType.MOBI
lowerName.endsWith(".fb2") || lowerName.endsWith(".fb2.zip") -> FileType.FB2
diff --git a/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
index f663779..8caee9c 100644
--- a/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
+++ b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
@@ -23,6 +23,7 @@ class MetadataExtractionWorker(
private val epubParser = EpubParser(appContext)
private val mobiParser = MobiParser(appContext)
private val pdfCoverGenerator = PdfCoverGenerator(appContext)
+ private val odtParser = com.aryan.reader.epub.OdtParser(appContext)
companion object {
const val WORK_NAME = "MetadataExtractionWorker"
@@ -109,6 +110,18 @@ class MetadataExtractionWorker(
}
title = item.displayName
}
+ FileType.ODT, FileType.FODT -> {
+ val book = odtParser.createOdtBook(
+ inputStream = inputStream,
+ bookId = item.bookId,
+ originalBookNameHint = item.displayName,
+ isFlat = type == FileType.FODT,
+ parseContent = false
+ )
+ title = book.title.takeIf { it.isNotBlank() && it != "content" }
+ author = book.author.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }
+ book.coverImage?.let { coverPath = recentFilesRepository.saveCoverToCache(it, uri) }
+ }
else -> {
title = item.displayName
}
diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
index 51c6864..7acead9 100644
--- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
@@ -24,17 +24,12 @@ import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Build
-import timber.log.Timber
+import android.speech.tts.TextToSpeech
import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.Switch
-import androidx.compose.material3.ModalBottomSheet
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -68,6 +63,8 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -85,10 +82,14 @@ import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SwapHoriz
+import androidx.compose.material.icons.filled.Tune
import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -99,11 +100,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -130,11 +137,13 @@ import com.aryan.reader.SearchState
import com.aryan.reader.SearchTopBar
import com.aryan.reader.TooltipIconButton
import com.aryan.reader.epub.EpubChapter
+import com.aryan.reader.loadNativeVoice
import com.aryan.reader.paginatedreader.BookPaginator
import com.aryan.reader.paginatedreader.IPaginator
import com.aryan.reader.tts.TtsPlaybackManager.TtsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import timber.log.Timber
import kotlin.math.roundToInt
enum class ReaderTool(val title: String, val category: String) {
@@ -504,6 +513,8 @@ fun EpubReaderBottomBar(
isTtsSessionActive: Boolean,
ttsState: TtsState,
isProUser: Boolean,
+ currentTtsMode: com.aryan.reader.tts.TtsPlaybackManager.TtsMode,
+ onOpenTtsControls: () -> Unit,
onOpenSlider: () -> Unit,
onOpenDrawer: () -> Unit,
onToggleFormat: () -> Unit,
@@ -658,6 +669,19 @@ fun EpubReaderBottomBar(
) else stringResource(R.string.content_desc_resume_tts)
)
}
+
+ if (currentTtsMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.BASE) {
+ TooltipIconButton(
+ text = "Voice Adjustments",
+ description = "Adjust voice speed and pitch",
+ onClick = onOpenTtsControls
+ ) {
+ Icon(
+ imageVector = Icons.Default.Tune,
+ contentDescription = "Voice Adjustments"
+ )
+ }
+ }
}
}
}
@@ -1421,4 +1445,198 @@ fun CustomizeToolsSheet(
}
}
}
+}
+
+@androidx.annotation.OptIn(UnstableApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TtsControlsSheet(
+ onDismiss: () -> Unit,
+ onOpenDeviceVoiceSettings: () -> Unit,
+ ttsController: com.aryan.reader.tts.TtsController
+) {
+ val context = androidx.compose.ui.platform.LocalContext.current
+ val ttsState by ttsController.ttsState.collectAsState()
+
+ // Local TTS for Sample Playback
+ var tts by remember { mutableStateOf(null) }
+ var isTtsReady by remember { mutableStateOf(false) }
+
+ var rate by remember { mutableFloatStateOf(loadTtsSpeechRate(context)) }
+ var pitch by remember { mutableFloatStateOf(loadTtsPitch(context)) }
+
+ var isDraggingRate by remember { mutableStateOf(false) }
+ var isDraggingPitch by remember { mutableStateOf(false) }
+
+ // Initialize Local TTS for samples
+ DisposableEffect(Unit) {
+ val instance = TextToSpeech(context) { status ->
+ if (status == TextToSpeech.SUCCESS) {
+ isTtsReady = true
+ try {
+ val preferredVoiceName = loadNativeVoice(context)
+ if (preferredVoiceName != null) {
+ tts?.voices?.find { it.name == preferredVoiceName }?.let { targetVoice ->
+ tts?.voice = targetVoice
+ }
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to apply preferred voice in sample")
+ }
+ }
+ }
+ tts = instance
+ onDispose { instance.shutdown() }
+ }
+
+ val saveAndSlice = {
+ saveTtsSpeechRate(context, rate)
+ saveTtsPitch(context, pitch)
+ ttsController.sliceAndRetainPosition()
+ }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ contentWindowInsets = { WindowInsets.navigationBars }
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) {
+ Text("Voice Adjustments", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+ Spacer(Modifier.height(16.dp))
+
+ // Rate Slider
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Speed (${"%.1f".format(rate)}x)", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium)
+ IconButton(onClick = {
+ rate = 1.0f
+ ttsController.pause()
+ saveAndSlice()
+ }) {
+ Icon(Icons.Default.Refresh, contentDescription = "Reset Speed")
+ }
+ }
+ Slider(
+ value = rate,
+ onValueChange = {
+ rate = it
+ // Pause playback immediately when user starts dragging
+ if (!isDraggingRate) {
+ isDraggingRate = true
+ ttsController.pause()
+ }
+ },
+ onValueChangeFinished = {
+ isDraggingRate = false
+ saveAndSlice()
+ },
+ valueRange = 0.5f..3.0f,
+ steps = 24 // Creates 0.1 increments
+ )
+
+ // Pitch Slider
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Pitch (${"%.1f".format(pitch)}x)", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium)
+ IconButton(onClick = {
+ pitch = 1.0f
+ ttsController.pause()
+ saveAndSlice()
+ }) {
+ Icon(Icons.Default.Refresh, contentDescription = "Reset Pitch")
+ }
+ }
+ Slider(
+ value = pitch,
+ onValueChange = {
+ pitch = it
+ if (!isDraggingPitch) {
+ isDraggingPitch = true
+ ttsController.pause()
+ }
+ },
+ onValueChangeFinished = {
+ isDraggingPitch = false
+ saveAndSlice()
+ },
+ valueRange = 0.5f..2.0f,
+ steps = 14 // Creates 0.1 increments
+ )
+
+ Spacer(Modifier.height(8.dp))
+
+ // Play Sample Button
+ Button(
+ onClick = {
+ if (ttsState.isPlaying) ttsController.pause()
+ tts?.setSpeechRate(rate)
+ tts?.setPitch(pitch)
+ tts?.speak("This is how your current voice settings sound.", TextToSpeech.QUEUE_FLUSH, null, null)
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = isTtsReady,
+ colors = androidx.compose.material3.ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ ) {
+ Icon(Icons.Default.GraphicEq, contentDescription = null)
+ Spacer(Modifier.width(8.dp))
+ Text("Play Sample")
+ }
+
+ Spacer(Modifier.height(24.dp))
+
+ // Central Play/Pause Control for the Book
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ FilledIconButton(
+ onClick = {
+ tts?.stop()
+ if (ttsState.isPlaying) ttsController.pause() else ttsController.resume()
+ },
+ modifier = Modifier.size(64.dp),
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ ) {
+ if (ttsState.isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(32.dp),
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ strokeWidth = 3.dp
+ )
+ } else {
+ Icon(
+ painter = painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
+ contentDescription = if (ttsState.isPlaying) "Pause Book" else "Play Book",
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ }
+ Spacer(Modifier.height(8.dp))
+ Text(
+ text = if (ttsState.isPlaying) "Pause Book" else "Resume Book",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Spacer(Modifier.height(24.dp))
+ OutlinedButton(
+ onClick = {
+ onDismiss()
+ onOpenDeviceVoiceSettings()
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Default.Settings, contentDescription = null)
+ Spacer(Modifier.width(8.dp))
+ Text("System Voice / Engine Settings")
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
index b1a9cfb..de7cbe0 100644
--- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
@@ -1042,6 +1042,7 @@ fun EpubReaderHost(
var showPermissionRationaleDialog by remember { mutableStateOf(false) }
var showTtsSettingsSheet by remember { mutableStateOf(false) }
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
+ var showTtsControlsSheet by remember { mutableStateOf(false) }
var showThemePanel by remember { mutableStateOf(false) }
var showPaletteManager by remember { mutableStateOf(false) }
@@ -3722,6 +3723,8 @@ fun EpubReaderHost(
ttsState = ttsState,
isProUser = isProUser,
hiddenTools = hiddenTools,
+ currentTtsMode = currentTtsMode,
+ onOpenTtsControls = { showTtsControlsSheet = true },
onOpenSlider = {
when (currentRenderMode) {
RenderMode.VERTICAL_SCROLL -> {
@@ -4176,6 +4179,14 @@ fun EpubReaderHost(
)
}
+ if (showTtsControlsSheet) {
+ TtsControlsSheet(
+ onDismiss = { showTtsControlsSheet = false },
+ onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true },
+ ttsController = ttsController
+ )
+ }
+
if (showCustomizeToolsSheet) {
CustomizeToolsSheet(
hiddenTools = hiddenTools,
diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt
index aa18c24..4cadb06 100644
--- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt
@@ -114,8 +114,30 @@ private const val PAGE_INFO_MODE_KEY = "reader_page_info_mode"
private const val PULL_TO_TURN_ENABLED_KEY = "reader_pull_to_turn_enabled"
const val DEFAULT_FONT_SIZE_VAL = 1.0f
-const val DEFAULT_LINE_HEIGHT_VAL = 1.6f
+const val DEFAULT_LINE_HEIGHT_VAL = 1.0f
const val DEFAULT_PARAGRAPH_GAP_VAL = 1.0f
+private const val TTS_SPEECH_RATE_KEY = "tts_speech_rate"
+private const val TTS_PITCH_KEY = "tts_pitch"
+
+fun saveTtsSpeechRate(context: Context, rate: Float) {
+ val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
+ prefs.edit { putFloat(TTS_SPEECH_RATE_KEY, rate) }
+}
+
+fun loadTtsSpeechRate(context: Context): Float {
+ val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
+ return prefs.getFloat(TTS_SPEECH_RATE_KEY, 1.0f)
+}
+
+fun saveTtsPitch(context: Context, pitch: Float) {
+ val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
+ prefs.edit { putFloat(TTS_PITCH_KEY, pitch) }
+}
+
+fun loadTtsPitch(context: Context): Float {
+ val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
+ return prefs.getFloat(TTS_PITCH_KEY, 1.0f)
+}
enum class ReaderFont(val id: String, val displayName: String, val fontFamilyName: String) {
ORIGINAL("original", "Original", "Original"),
@@ -559,7 +581,7 @@ fun ReaderTextFormatPanel(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Size
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- Text("Size", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(60.dp))
+ Text("Font Size", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp))
Slider(
value = currentFontSize,
onValueChange = onFontSizeChange,
@@ -567,31 +589,31 @@ fun ReaderTextFormatPanel(
steps = 24,
modifier = Modifier.weight(1f)
)
- Text("%.1fx".format(currentFontSize), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End)
+ Text(if (currentFontSize in 0.99f..1.01f) "Orig" else "%.1fx".format(currentFontSize), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End)
}
// Lines
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- Text("Lines", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(60.dp))
+ Text("Line Height", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp))
Slider(
value = currentLineHeight,
onValueChange = onLineHeightChange,
- valueRange = 1.0f..2.5f,
- steps = 14,
+ valueRange = 1.0f..3.0f,
+ steps = 19,
modifier = Modifier.weight(1f)
)
- Text("%.1fx".format(currentLineHeight), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End)
+ Text(if (currentLineHeight <= 1.01f) "Orig" else "%.1fx".format(currentLineHeight), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End)
}
// Paragraph Gap
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- Text("Gap", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(60.dp))
+ Text("Paragraph Gap", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp))
Slider(
value = currentParagraphGap,
onValueChange = onParagraphGapChange,
valueRange = 0.0f..3.0f,
- steps = 12,
+ steps = 29,
modifier = Modifier.weight(1f)
)
- Text("%.1fx".format(currentParagraphGap), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End)
+ Text(if (currentParagraphGap in 0.99f..1.01f) "Orig" else "%.1fx".format(currentParagraphGap), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End)
}
}
Spacer(Modifier.height(8.dp))
diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt
index a17bcae..26cd25e 100644
--- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt
+++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt
@@ -1145,6 +1145,26 @@ private fun getHighlightOffsetsInBlock(
return null
}
+private fun List.extractTextBlocks(): List {
+ val result = mutableListOf()
+ for (block in this) {
+ when (block) {
+ is WrappingContentBlock -> result.addAll(block.paragraphsToWrap)
+ is FlexContainerBlock -> result.addAll(block.children.extractTextBlocks())
+ is TableBlock -> {
+ block.rows.forEach { row ->
+ row.forEach { cell ->
+ result.addAll(cell.content.extractTextBlocks())
+ }
+ }
+ }
+ is TextContentBlock -> result.add(block)
+ else -> {}
+ }
+ }
+ return result
+}
+
@Composable
private fun TextWithEmphasis(
text: AnnotatedString,
@@ -1577,13 +1597,22 @@ internal fun PaginatedReaderContent(
}
val textBlocksOnPage =
- pageContent?.content?.filterIsInstance()
+ pageContent?.content?.extractTextBlocks()
?.filter { it.cfi != null } ?: emptyList()
val lastTextBlock = textBlocksOnPage.lastOrNull()
+ val lastBlockAbs = lastTextBlock?.let {
+ when (it) {
+ is ParagraphBlock -> it.startCharOffsetInSource
+ is HeaderBlock -> it.startCharOffsetInSource
+ is QuoteBlock -> it.startCharOffsetInSource
+ is ListItemBlock -> it.startCharOffsetInSource
+ }
+ }
- // Strict Trigger Check - Custom Selection
LaunchedEffect(activeSelection, lastTextBlock, isDraggingHandle) {
- if (isDraggingHandle && activeSelection != null && lastTextBlock != null && activeSelection!!.endBlockIndex == lastTextBlock.blockIndex) {
+ if (isDraggingHandle && activeSelection != null && lastTextBlock != null &&
+ activeSelection!!.endBlockIndex == lastTextBlock.blockIndex &&
+ activeSelection!!.endBlockCharOffset == lastBlockAbs) {
if (activeSelection!!.endOffset >= lastTextBlock.content.text.length - 3) {
if (crossPageTriggerInfo?.first != pageIndex) {
Timber.tag("TextSelectionDiag")
@@ -1607,7 +1636,7 @@ internal fun PaginatedReaderContent(
val content = pageContent ?: return@LaunchedEffect
val firstTextBlock =
- content.content.filterIsInstance()
+ content.content.extractTextBlocks()
.firstOrNull { it.cfi != null } ?: run {
pendingCrossPageSelection = null
return@LaunchedEffect
diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt b/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt
index c14a97e..5eea909 100644
--- a/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt
+++ b/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt
@@ -120,9 +120,12 @@ import com.aryan.reader.pdf.data.VirtualPage
import com.aryan.reader.pdf.ocr.OcrElement
import com.aryan.reader.pdf.ocr.OcrResult
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -169,7 +172,7 @@ data class EmbeddedAnnotation(
data class PdfPoint(val x: Float, val y: Float, val timestamp: Long = 0L)
-data class PdfTile(val bitmap: Bitmap, val renderRect: Rect, val tileId: Int)
+data class PdfTile(val bitmap: Bitmap, val renderRect: Rect, val tileId: Int, val renderScale: Float = 1f)
enum class LinkSource {
ANNOTATION, TEXT_CONTENT
@@ -371,6 +374,7 @@ data class PageSelectionData(
val customHighlightColors: StableHolder