nice ui and ux ig

This commit is contained in:
inhale-dir
2024-12-13 11:49:43 +01:00
parent e57d8b7490
commit 04820240dc
8 changed files with 859 additions and 515 deletions
+39 -1
View File
@@ -79,7 +79,6 @@ dependencies {
exclude(group = "xmlpull", module = "xmlpull") exclude(group = "xmlpull", module = "xmlpull")
} }
implementation("org.slf4j:slf4j-android:1.7.36") implementation("org.slf4j:slf4j-android:1.7.36")
implementation("org.jsoup:jsoup:1.15.3")
implementation("net.sf.kxml:kxml2:2.3.0") implementation("net.sf.kxml:kxml2:2.3.0")
// Add material icons extended for the settings icon // Add material icons extended for the settings icon
@@ -93,4 +92,43 @@ dependencies {
// Add Timber for logging // Add Timber for logging
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
// Add Material Design Icons Extended
implementation("androidx.compose.material:material-icons-extended:1.6.1")
// Add Compose Animation libraries
implementation("androidx.compose.animation:animation:1.6.1")
implementation("androidx.compose.animation:animation-graphics:1.6.1")
// Add Accompanist for UI utilities
implementation("com.google.accompanist:accompanist-systemuicontroller:0.34.0")
implementation("com.google.accompanist:accompanist-placeholder-material:0.34.0")
// Add foundation dependency using BOM version management
implementation(platform(libs.androidx.compose.bom))
implementation("androidx.compose.foundation:foundation")
// Add Jsoup for HTML processing
implementation("org.jsoup:jsoup:1.16.2")
// Add epublib for epub handling
implementation("com.positiondev.epublib:epublib-core:3.1")
// Add Compose Animation dependencies
implementation(platform(libs.androidx.compose.bom))
implementation("androidx.compose.animation:animation")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
configurations.all {
resolutionStrategy {
// Exclude conflicting SLF4J implementations
exclude(group = "org.slf4j", module = "slf4j-simple")
// Exclude conflicting XML pull parser
exclude(group = "xmlpull", module = "xmlpull")
}
}
} }
+274 -132
View File
@@ -15,6 +15,8 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -35,13 +37,64 @@ import kotlinx.coroutines.launch
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.navArgument import androidx.navigation.navArgument
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.lazy.grid.rememberLazyGridState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.text.style.TextOverflow
// import androidx.navigation.compose.NavHostController
import inhale.rip.epook.data.SettingsStore
import androidx.navigation.compose.currentBackStackEntryAsState
// Add settings route // Add these imports at the top
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
// import androidx.compose.ui.text.style.TextAlign
// Add sealed class for navigation routes
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
object BookList : Screen("books") object BookList : Screen("bookList")
object Reader : Screen("reader/{bookId}")
object Settings : Screen("settings") object Settings : Screen("settings")
object Reader : Screen("reader/{bookId}") {
fun createRoute(bookId: String) = "reader/$bookId"
}
} }
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -49,9 +102,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
EpookTheme { App()
App()
}
} }
} }
} }
@@ -59,29 +110,34 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun App() { fun App() {
val navController = rememberNavController() val navController = rememberNavController()
val context = LocalContext.current
val settingsStore = remember(context) { SettingsStore(context) }
NavHost(navController = navController, startDestination = Screen.BookList.route) { EpookTheme {
composable(Screen.BookList.route) { NavHost(navController = navController, startDestination = Screen.BookList.route) {
BookshelfScreen( composable(Screen.BookList.route) {
onBookClick = { bookId -> BookshelfScreen(
navController.navigate("reader/$bookId") onBookClick = { bookId ->
} navController.navigate(Screen.Reader.createRoute(bookId))
) }
} )
composable( }
route = "reader/{bookId}", composable(
arguments = listOf(navArgument("bookId") { type = NavType.StringType }) route = Screen.Reader.route,
) { backStackEntry -> arguments = listOf(navArgument("bookId") { type = NavType.StringType })
ReaderScreen( ) { backStackEntry ->
bookId = backStackEntry.arguments?.getString("bookId") ?: "", val bookId = backStackEntry.arguments?.getString("bookId") ?: ""
onNavigateBack = { navController.navigateUp() }, ReaderScreen(
onOpenSettings = { navController.navigate(Screen.Settings.route) } bookId = bookId,
) onNavigateBack = { navController.navigateUp() }
} )
composable(Screen.Settings.route) { }
SettingsScreen( composable(Screen.Settings.route) {
onNavigateBack = { navController.navigateUp() } SettingsScreen(
) onNavigateBack = { navController.navigateUp() },
settingsStore = settingsStore
)
}
} }
} }
} }
@@ -96,88 +152,92 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var bookToDelete by remember { mutableStateOf<Book?>(null) } var bookToDelete by remember { mutableStateOf<Book?>(null) }
var showEmptyState by remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent() contract = ActivityResultContracts.GetContent()
) { uri: Uri? -> ) { uri: Uri? ->
uri?.let { selectedUri -> uri?.let { selectedUri ->
context.contentResolver.openInputStream(selectedUri)?.use { inputStream -> try {
try { val inputStream = context.contentResolver.openInputStream(selectedUri)
val bookId = System.currentTimeMillis().toString() val book = EpubReader().readEpub(inputStream)
val epubFile = File(context.filesDir, "book_$bookId.epub")
val bookId = java.util.UUID.randomUUID().toString()
println("Saving book to: ${epubFile.absolutePath}")
val epubFile = File(context.filesDir, "book_$bookId.epub")
// Use buffered streams for better performance context.contentResolver.openInputStream(selectedUri)?.use { input ->
inputStream.buffered().use { bufferedInput -> FileOutputStream(epubFile).use { output ->
FileOutputStream(epubFile).buffered().use { bufferedOutput -> input.copyTo(output)
bufferedInput.copyTo(bufferedOutput)
}
} }
println("Book file saved, size: ${epubFile.length()} bytes")
// Verify the file exists and is readable
if (!epubFile.exists() || !epubFile.canRead()) {
println("Error: File not accessible after saving")
return@use
}
// Test reading the EPUB
epubFile.inputStream().buffered().use { fileInput ->
val epubReader = EpubReader()
val book = epubReader.readEpub(fileInput)
println("Book loaded for verification: ${book.title}")
println("Number of spine refs: ${book.spine.spineReferences.size}")
// Extract and save cover image if it exists
val coverFile = book.coverImage?.let { coverImage ->
File(context.filesDir, "cover_$bookId.jpg").also { file ->
FileOutputStream(file).buffered().use { output ->
output.write(coverImage.data)
}
}
}
// Add the book to the store
scope.launch {
bookStore.addBook(Book(
id = bookId,
title = book.title ?: "Unknown Title",
coverImageFile = coverFile,
filePath = epubFile.absolutePath
))
}
}
} catch (e: Exception) {
println("Error saving book: ${e.message}")
e.printStackTrace()
} }
val coverFile = book.coverImage?.let { coverImage ->
File(context.filesDir, "cover_$bookId.jpg").also { file ->
FileOutputStream(file).use { output ->
output.write(coverImage.data)
}
}
}
scope.launch {
bookStore.addBook(Book(
id = bookId,
title = book.title ?: "Unknown Title",
coverImageFile = coverFile,
filePath = epubFile.absolutePath
))
}
} catch (e: Exception) {
println("Error saving book: ${e.message}")
e.printStackTrace()
} }
} }
} }
LaunchedEffect(books) {
showEmptyState = books.isEmpty()
}
if (showDeleteDialog && bookToDelete != null) { if (showDeleteDialog && bookToDelete != null) {
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteDialog = false }, onDismissRequest = { showDeleteDialog = false },
title = { Text("Delete Book") }, icon = {
text = { Text("Are you sure you want to delete '${bookToDelete?.title}'?") }, Icon(
imageVector = Icons.Filled.Delete,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text(
"Delete Book",
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
"Are you sure you want to delete '${bookToDelete?.title}'? This action cannot be undone.",
style = MaterialTheme.typography.bodyLarge
)
},
confirmButton = { confirmButton = {
TextButton(onClick = { Button(
scope.launch { onClick = {
bookToDelete?.let { bookStore.deleteBook(it.id) } scope.launch {
} bookToDelete?.let { bookStore.deleteBook(it.id) }
showDeleteDialog = false showDeleteDialog = false
bookToDelete = null bookToDelete = null
}) { }
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Delete") Text("Delete")
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { OutlinedButton(onClick = { showDeleteDialog = false }) {
showDeleteDialog = false
bookToDelete = null
}) {
Text("Cancel") Text("Cancel")
} }
} }
@@ -185,42 +245,97 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
} }
Scaffold( Scaffold(
topBar = {
LargeTopAppBar(
title = {
Column {
Text(
"Library",
style = MaterialTheme.typography.headlineLarge
)
AnimatedVisibility(
visible = !showEmptyState,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Text(
"${books.size} book${if (books.size != 1) "s" else ""}",
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
)
}
}
},
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
)
)
},
floatingActionButton = { floatingActionButton = {
FloatingActionButton(onClick = { launcher.launch("application/epub+zip") }) { FloatingActionButton(
onClick = { launcher.launch("application/epub+zip") },
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) {
Icon(Icons.Default.Add, contentDescription = "Add book") Icon(Icons.Default.Add, contentDescription = "Add book")
} }
} }
) { padding -> ) { padding ->
LazyVerticalGrid( Box(
columns = GridCells.Adaptive(minSize = 128.dp), modifier = Modifier
contentPadding = padding, .fillMaxSize()
modifier = Modifier.fillMaxSize() .padding(padding)
) { ) {
items(books) { book -> if (showEmptyState) {
Card( Column(
modifier = Modifier modifier = Modifier
.padding(8.dp) .fillMaxSize()
.combinedClickable( .padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.MenuBook,
contentDescription = null,
modifier = Modifier.size(96.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Your library is empty",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Tap the + button to add your first book",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
textAlign = TextAlign.Center
)
}
} else {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize()
) {
items(
items = books,
key = { it.id }
) { book ->
BookCard(
book = book,
onClick = { onBookClick(book.id) }, onClick = { onBookClick(book.id) },
onLongClick = { onLongClick = {
bookToDelete = book bookToDelete = book
showDeleteDialog = true showDeleteDialog = true
} },
) modifier = Modifier.animateItemPlacement()
) {
Column {
AsyncImage(
model = book.coverImageFile ?: R.drawable.ic_launcher_background,
contentDescription = book.title,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.7f),
contentScale = ContentScale.Crop
)
Text(
text = book.title,
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.bodyMedium
) )
} }
} }
@@ -229,27 +344,54 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BookCover(book: Book, onClick: () -> Unit) { private fun BookCard(
Card( book: Book,
modifier = Modifier onClick: () -> Unit,
.padding(8.dp) onLongClick: () -> Unit,
.clickable(onClick = onClick) modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
ElevatedCard(
modifier = modifier
.fillMaxWidth()
.aspectRatio(0.7f)
.pointerInput(Unit) {
detectTapGestures(
onTap = { onClick() },
onLongPress = { onLongClick() }
)
},
elevation = CardDefaults.elevatedCardElevation(
defaultElevation = 4.dp,
pressedElevation = if (isPressed) 8.dp else 4.dp
)
) { ) {
Column { Box(modifier = Modifier.fillMaxSize()) {
AsyncImage( AsyncImage(
model = book.coverImageFile ?: R.drawable.ic_launcher_background, model = book.coverImageFile ?: R.drawable.ic_launcher_background,
contentDescription = book.title, contentDescription = book.title,
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxWidth()
.aspectRatio(0.7f),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Text(
text = book.title, Surface(
modifier = Modifier.padding(8.dp), modifier = Modifier
style = MaterialTheme.typography.bodyMedium .fillMaxWidth()
) .align(Alignment.BottomCenter),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
) {
Text(
text = book.title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(12.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
} }
} }
} }
+346 -234
View File
@@ -2,290 +2,402 @@ package inhale.rip.epook
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.launch
import nl.siegmann.epublib.domain.Book
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import java.io.File
import inhale.rip.epook.data.SettingsStore
import inhale.rip.epook.data.BookStore import inhale.rip.epook.data.BookStore
import androidx.compose.foundation.gestures.detectDragGestures import inhale.rip.epook.data.SettingsStore
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import timber.log.Timber
import java.nio.charset.Charset
import nl.siegmann.epublib.domain.MediaType
import nl.siegmann.epublib.domain.Resource
// Update the JavaScript for accurate page calculation
private const val PAGE_CALCULATION_JS = """
(function() {
const content = document.body;
const contentWidth = content.scrollWidth;
const pageWidth = window.innerWidth;
const totalPages = Math.max(1, Math.ceil(contentWidth / pageWidth));
console.log('Content width:', contentWidth, 'Page width:', pageWidth, 'Total pages:', totalPages);
return totalPages;
})();
"""
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderScreen( fun ReaderScreen(
bookId: String, bookId: String,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit
onOpenSettings: () -> Unit
) { ) {
var book by remember { mutableStateOf<Book?>(null) }
var currentHtml by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) }
var currentPageIndex by remember { mutableStateOf(0) }
var totalPages by remember { mutableStateOf(0) }
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val settingsStore = remember(context) { SettingsStore(context) }
val fontFamily by settingsStore.fontFamily.collectAsState(initial = "Georgia")
val fontSize by settingsStore.fontSize.collectAsState(initial = 16)
val lineHeight by settingsStore.lineHeight.collectAsState(initial = 1.6f)
val bookStore = remember { BookStore(context) } val bookStore = remember { BookStore(context) }
val savedPosition by bookStore.getReadingPosition(bookId).collectAsState(initial = 0) val settingsStore = remember { SettingsStore(context) }
val scope = rememberCoroutineScope()
var showControls by remember { mutableStateOf(true) }
var showSettings by remember { mutableStateOf(false) }
var brightness by remember { mutableStateOf(1f) }
var isDarkMode by remember { mutableStateOf(false) }
var fontSize by remember { mutableStateOf(16f) }
var lineHeight by remember { mutableStateOf(1.5f) }
var padding by remember { mutableStateOf(16f) }
var fontFamily by remember { mutableStateOf("Roboto") }
var currentChapter by remember { mutableStateOf(0) }
var showChapterList by remember { mutableStateOf(false) }
var chapters by remember { mutableStateOf(listOf<String>()) }
var currentPage by remember { mutableStateOf(1) }
var totalPages by remember { mutableStateOf(1) }
var bookContent by remember { mutableStateOf("") }
var webViewRef by remember { mutableStateOf<WebView?>(null) }
// Function to load content for a specific page // Move processChapterHtml outside the Composable
fun loadPage(pageIndex: Int) { fun processChapterHtml(rawHtml: String, chapterIndex: Int): String {
scope.launch { val document = Jsoup.parse(rawHtml)
try { val body = document.body()
book?.spine?.spineReferences?.getOrNull(pageIndex)?.let { ref -> body.select("script, style").remove()
val resource = ref.resource return "<div class='chapter' id='chapter_$chapterIndex'>\n${body.html()}\n</div>"
val html = String(resource.data, Charsets.UTF_8) }
val doc = Jsoup.parse(html)
// Load book content
LaunchedEffect(bookId) {
try {
Timber.d("📚 Starting to load book with ID: $bookId")
val book = bookStore.getBook(bookId)
Timber.d("📚 Book loaded successfully: ${book.title}")
// Load settings first
val savedSettings = settingsStore.loadSettings()
currentPage = savedSettings.currentPage
fontSize = savedSettings.fontSize
lineHeight = savedSettings.lineHeight
padding = savedSettings.padding
fontFamily = savedSettings.fontFamily
isDarkMode = savedSettings.isDarkMode
Timber.d("📚 Settings loaded - Page: $currentPage, Font: $fontSize, Line: $lineHeight, Padding: $padding")
// Process chapters
val processedChapters = mutableListOf<String>()
book.spine.spineReferences.forEachIndexed { index, spineReference ->
Timber.d("📚 Processing chapter $index")
val resource = book.resources.getByHref(spineReference.resource.href)
Timber.d("📚 Resource href: ${resource.href}")
Timber.d("📚 Resource media type: ${resource.mediaType}")
if (resource.mediaType == MediaType.XHTML) {
val rawHtml = String(resource.data, Charset.forName("UTF-8"))
Timber.d("📚 Raw HTML length: ${rawHtml.length}")
// Remove unnecessary elements but keep styling val processedHtml = processChapterHtml(rawHtml, index)
doc.select("script").remove() processedChapters.add(processedHtml)
Timber.d("📚 Chapter $index processed successfully")
// Add custom CSS for better reading experience
val style = doc.head().appendElement("style")
style.appendText("""
body {
font-family: '$fontFamily', serif;
font-size: ${fontSize}px;
line-height: $lineHeight;
padding: 20px;
max-width: 800px;
margin: 0 auto;
color: #333;
background-color: #f8f8f8;
}
p {
margin: 1em 0;
text-align: justify;
}
h1, h2, h3, h4, h5, h6 {
color: #2c3e50;
margin: 1.5em 0 0.5em;
}
img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #e0e0e0;
}
h1, h2, h3, h4, h5, h6 {
color: #b0b0b0;
}
}
""".trimIndent())
currentHtml = doc.outerHtml()
println("Loaded page $pageIndex")
// Save the position whenever we load a new page
bookStore.updateReadingPosition(bookId, pageIndex)
} ?: run {
errorMessage = "Page not found"
} }
} catch (e: Exception) {
println("Error loading page: ${e.message}")
errorMessage = "Error loading page: ${e.message}"
} }
// Combine all chapters
bookContent = processedChapters.joinToString("\n")
Timber.d("📚 Final book content processed:")
Timber.d("📚 - Total length: ${bookContent.length}")
Timber.d("📚 - First 100 chars: ${bookContent.take(100)}")
Timber.d("📚 - Last 100 chars: ${bookContent.takeLast(100)}")
// Update chapter list
chapters = book.tableOfContents.tocReferences.map { it.title }
} catch (e: Exception) {
Timber.e(e, "📚 Error loading book")
} }
} }
LaunchedEffect(bookId) { val css = """
scope.launch { :root {
try { --page-width: calc(100vw - ${padding * 2}px);
val bookFile = File(context.filesDir, "book_$bookId.epub") }
if (!bookFile.exists()) { body {
errorMessage = "Book file not found" margin: 0;
return@launch padding: ${padding}px;
} width: var(--page-width);
max-width: var(--page-width);
height: calc(100vh - ${padding * 2}px);
column-width: var(--page-width);
column-gap: ${padding * 2}px;
column-fill: auto;
overflow-x: hidden;
overflow-y: hidden;
font-family: $fontFamily;
font-size: ${fontSize}px;
line-height: ${lineHeight}em;
background-color: ${if (isDarkMode) "#1C1B1F" else "#FFFFFF"};
color: ${if (isDarkMode) "#E6E1E5" else "#1C1B1F"};
}
.chapter {
break-inside: avoid;
margin-bottom: 2em;
display: inline-block;
width: 100%;
}
img {
max-width: 100%;
height: auto;
}
p {
margin: 0.5em 0;
text-align: justify;
}
""".trimIndent()
bookFile.inputStream().buffered().use { inputStream -> BackHandler(enabled = showSettings || showChapterList) {
val epubReader = EpubReader() when {
book = epubReader.readEpub(inputStream) showSettings -> showSettings = false
totalPages = book?.spine?.spineReferences?.size ?: 0 showChapterList -> showChapterList = false
println("Book loaded: ${book?.title}, total pages: $totalPages")
// Use saved position instead of always starting at the beginning
currentPageIndex = savedPosition
loadPage(currentPageIndex)
}
} catch (e: Exception) {
errorMessage = "Error loading book: ${e.message}"
}
} }
} }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( AnimatedVisibility(
title = { Text(book?.title ?: "Loading...") }, visible = showControls,
navigationIcon = { enter = slideInVertically() + fadeIn(),
IconButton(onClick = onNavigateBack) { exit = slideOutVertically() + fadeOut()
Icon(Icons.Default.ArrowBack, contentDescription = "Back") ) {
TopAppBar(
title = {
Column {
Text("Chapter ${currentChapter + 1}")
Text(
"Page $currentPage of $totalPages",
style = MaterialTheme.typography.bodyMedium
)
}
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
},
actions = {
IconButton(onClick = { showChapterList = true }) {
Icon(Icons.Default.List, "Chapters")
}
IconButton(onClick = { showSettings = true }) {
Icon(Icons.Default.Settings, "Settings")
}
IconButton(onClick = { isDarkMode = !isDarkMode }) {
Icon(
if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode,
"Toggle theme"
)
}
} }
}, )
actions = { }
IconButton(onClick = onOpenSettings) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
)
} }
) { padding -> ) { padding ->
Box( Box(modifier = Modifier.padding(padding)) {
modifier = Modifier AndroidView(
.fillMaxSize() factory = { context ->
.padding(padding) WebView(context).apply {
) { settings.apply {
when { javaScriptEnabled = true
errorMessage != null -> { useWideViewPort = true
Column( loadWithOverviewMode = true
modifier = Modifier defaultFontSize = fontSize.toInt()
.align(Alignment.Center) Timber.d("📚 WebView settings configured")
.padding(16.dp), }
horizontalAlignment = Alignment.CenterHorizontally
) { webViewClient = object : WebViewClient() {
Icon( override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
imageVector = Icons.Default.ArrowBack, super.onPageStarted(view, url, favicon)
contentDescription = null, Timber.d("📚 WebView page load started")
tint = MaterialTheme.colorScheme.error }
)
Spacer(modifier = Modifier.height(8.dp)) override fun onPageFinished(view: WebView?, url: String?) {
Text( super.onPageFinished(view, url)
text = errorMessage ?: "Unknown error", Timber.d("📚 WebView page load finished")
color = MaterialTheme.colorScheme.error
) // Add a slight delay to ensure content is fully laid out
} view?.postDelayed({
} Timber.d("📚 Starting page calculation")
currentHtml == null -> { view.evaluateJavascript(PAGE_CALCULATION_JS) { result ->
Column( try {
modifier = Modifier.align(Alignment.Center), val pages = result.toInt()
horizontalAlignment = Alignment.CenterHorizontally Timber.d("📚 Page calculation complete - Total pages: $pages")
) { totalPages = pages
CircularProgressIndicator() } catch (e: Exception) {
Spacer(modifier = Modifier.height(16.dp)) Timber.e(e, "📚 Error calculating pages")
Text("Loading book...")
}
}
else -> {
Column(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
val (x, _) = dragAmount
when {
x > 50 && currentPageIndex > 0 -> {
currentPageIndex--
loadPage(currentPageIndex)
}
x < -50 && currentPageIndex < totalPages - 1 -> {
currentPageIndex++
loadPage(currentPageIndex)
} }
} }
// Log the current HTML content for debugging
view.evaluateJavascript(
"(function() { return document.documentElement.outerHTML; })()",
) { result ->
Timber.d("📚 Current HTML content length: ${result.length}")
Timber.d("📚 First 100 chars of HTML: ${result.take(100)}")
}
// Scroll to last position
view.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'auto'})",
null
)
}, 500)
}
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String?
) {
super.onReceivedError(view, errorCode, description, failingUrl)
Timber.e("📚 WebView error: $errorCode - $description")
}
}
Timber.d("📚 Loading content into WebView")
loadDataWithBaseURL(
null,
"""
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>$css</style>
</head>
<body>$bookContent</body>
</html>
""".trimIndent(),
"text/html",
"UTF-8",
null
)
webViewRef = this
}
},
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
val screenWidth = this.size.width.toFloat()
when {
offset.x < screenWidth * 0.3f && currentPage > 1 -> {
currentPage--
webViewRef?.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'smooth'})",
null
)
}
offset.x > screenWidth * 0.7f && currentPage < totalPages -> {
currentPage++
webViewRef?.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'smooth'})",
null
)
}
else -> {
showControls = !showControls
} }
} }
}
}
)
// Settings sheet
if (showSettings) {
ModalBottomSheet(
onDismissRequest = { showSettings = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) { ) {
// WebView for rendering HTML content Text("Reading Settings", style = MaterialTheme.typography.titleLarge)
AndroidView( Spacer(modifier = Modifier.height(16.dp))
factory = { context ->
WebView(context).apply { Text("Font Size: $fontSize", style = MaterialTheme.typography.bodyMedium)
settings.apply { Slider(
javaScriptEnabled = false value = fontSize,
builtInZoomControls = true onValueChange = {
displayZoomControls = false fontSize = it
useWideViewPort = true // Update WebView font size
loadWithOverviewMode = true webViewRef?.evaluateJavascript(
} "document.body.style.fontSize = '${fontSize}px'",
webViewClient = WebViewClient()
setBackgroundColor(android.graphics.Color.TRANSPARENT)
}
},
modifier = Modifier.weight(1f),
update = { webView ->
webView.loadDataWithBaseURL(
null,
currentHtml ?: "",
"text/html",
"UTF-8",
null null
) )
} },
valueRange = 12f..24f,
steps = 11
) )
// Navigation controls // Similar sliders for lineHeight, padding, brightness
Row( // Font family selector
modifier = Modifier // Color theme selector
.fillMaxWidth() }
.padding(vertical = 8.dp), }
horizontalArrangement = Arrangement.SpaceBetween, }
verticalAlignment = Alignment.CenterVertically
) { // Chapter list
IconButton( if (showChapterList) {
onClick = { ModalBottomSheet(
if (currentPageIndex > 0) { onDismissRequest = { showChapterList = false }
currentPageIndex-- ) {
loadPage(currentPageIndex) LazyColumn(
} modifier = Modifier.fillMaxWidth()
}, ) {
enabled = currentPageIndex > 0 items(chapters) { chapter ->
) { ListItem(
Icon( headlineContent = { Text(chapter) },
imageVector = Icons.Default.ArrowBack, modifier = Modifier.clickable {
contentDescription = "Previous page" // Navigate to chapter
) webViewRef?.evaluateJavascript(
} "document.getElementById('chapter_$currentChapter').scrollIntoView()",
null
Text( )
text = "Page ${currentPageIndex + 1} of $totalPages", showChapterList = false
style = MaterialTheme.typography.bodySmall }
) )
IconButton(
onClick = {
if (currentPageIndex < totalPages - 1) {
currentPageIndex++
loadPage(currentPageIndex)
}
},
enabled = currentPageIndex < totalPages - 1
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Next page",
modifier = Modifier.rotate(180f)
)
}
} }
} }
} }
} }
// Brightness overlay
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 1f - brightness))
)
}
}
// Save reading progress when leaving
DisposableEffect(Unit) {
onDispose {
scope.launch {
settingsStore.saveLastPosition(bookId, currentPage)
}
} }
} }
} }
@@ -2,7 +2,7 @@ package inhale.rip.epook
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -14,15 +14,21 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
onNavigateBack: () -> Unit onNavigateBack: () -> Unit,
settingsStore: SettingsStore
) { ) {
val context = LocalContext.current
val settingsStore = remember { SettingsStore(context) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var fontSize by remember { mutableStateOf(16f) }
var lineHeight by remember { mutableStateOf(1.5f) }
var padding by remember { mutableStateOf(16f) }
var currentFontFamily by remember { mutableStateOf("Roboto") }
val fontFamily by settingsStore.fontFamily.collectAsState(initial = "Georgia") LaunchedEffect(Unit) {
val fontSize by settingsStore.fontSize.collectAsState(initial = 16) fontSize = settingsStore.getFontSize()
val lineHeight by settingsStore.lineHeight.collectAsState(initial = 1.6f) lineHeight = settingsStore.getLineHeight()
padding = settingsStore.getPadding()
currentFontFamily = settingsStore.getFontFamily()
}
val fontFamilies = listOf("Georgia", "Roboto", "Times New Roman", "Arial", "Verdana") val fontFamilies = listOf("Georgia", "Roboto", "Times New Roman", "Arial", "Verdana")
@@ -32,16 +38,16 @@ fun SettingsScreen(
title = { Text("Settings") }, title = { Text("Settings") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
} }
) )
} }
) { padding -> ) { contentPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(contentPadding)
.padding(16.dp) .padding(16.dp)
) { ) {
Text( Text(
@@ -57,8 +63,9 @@ fun SettingsScreen(
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
) { ) {
RadioButton( RadioButton(
selected = font == fontFamily, selected = currentFontFamily == font,
onClick = { onClick = {
currentFontFamily = font
scope.launch { scope.launch {
settingsStore.updateFontFamily(font) settingsStore.updateFontFamily(font)
} }
@@ -78,10 +85,11 @@ fun SettingsScreen(
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Slider( Slider(
value = fontSize.toFloat(), value = fontSize,
onValueChange = { onValueChange = {
fontSize = it
scope.launch { scope.launch {
settingsStore.updateFontSize(it.toInt()) settingsStore.updateFontSize(it)
} }
}, },
valueRange = 12f..24f, valueRange = 12f..24f,
@@ -97,6 +105,7 @@ fun SettingsScreen(
Slider( Slider(
value = lineHeight, value = lineHeight,
onValueChange = { onValueChange = {
lineHeight = it
scope.launch { scope.launch {
settingsStore.updateLineHeight(it) settingsStore.updateLineHeight(it)
} }
@@ -104,6 +113,24 @@ fun SettingsScreen(
valueRange = 1f..2f, valueRange = 1f..2f,
steps = 9 steps = 9
) )
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Padding: $padding",
style = MaterialTheme.typography.titleMedium
)
Slider(
value = padding,
onValueChange = {
padding = it
scope.launch {
settingsStore.updatePadding(it)
}
},
valueRange = 8f..32f,
steps = 11
)
} }
} }
} }
@@ -9,41 +9,29 @@ import kotlinx.coroutines.flow.map
import java.io.File import java.io.File
import inhale.rip.epook.data.AppDatabase import inhale.rip.epook.data.AppDatabase
import android.util.Log import android.util.Log
import nl.siegmann.epublib.domain.Book as EpubBook
import nl.siegmann.epublib.epub.EpubReader
import java.io.FileInputStream
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "books") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "books")
class BookStore(private val context: Context) { class BookStore(private val context: Context) {
private val TAG = "BookStore" // Tag for logging private val TAG = "BookStore"
private val bookIdsKey = stringSetPreferencesKey("book_ids") private val bookIdsKey = stringSetPreferencesKey("book_ids")
private val bookDao = AppDatabase.getDatabase(context).bookDao() private val bookDao = AppDatabase.getDatabase(context).bookDao()
fun getAllBooks(): Flow<List<Book>> = bookDao.getAllBooks().map { entities -> fun getAllBooks(): Flow<List<Book>> = bookDao.getAllBooks().map { entities ->
entities.mapNotNull { entity -> entities.map { it.toBook() }
try {
val bookFile = File(entity.filePath)
if (bookFile.exists()) {
entity.toBook()
} else {
// If the file doesn't exist, delete the database entry
Log.d(TAG, "Book file missing for ${entity.id}, cleaning up database entry")
null
}
} catch (e: Exception) {
Log.e(TAG, "Error loading book ${entity.id}", e)
null
}
}
} }
suspend fun addBook(book: Book) { suspend fun addBook(book: Book) {
context.dataStore.edit { preferences -> bookDao.insertBook(BookEntity(
val currentIds = preferences[bookIdsKey]?.toMutableSet() ?: mutableSetOf() id = book.id,
currentIds.add(book.id) title = book.title,
preferences[bookIdsKey] = currentIds coverImagePath = book.coverImageFile?.absolutePath,
preferences[stringPreferencesKey("title_${book.id}")] = book.title filePath = book.filePath,
} readingPosition = 0
// Also add to Room database ))
bookDao.insertBook(book.toEntity())
} }
suspend fun updateReadingPosition(bookId: String, position: Int) { suspend fun updateReadingPosition(bookId: String, position: Int) {
@@ -55,11 +43,10 @@ class BookStore(private val context: Context) {
} }
suspend fun deleteBook(bookId: String) { suspend fun deleteBook(bookId: String) {
Log.e(TAG, "🔥 DELETE BOOK STARTED 🔥") // Very visible error log Log.e(TAG, "🔥 DELETE BOOK STARTED 🔥")
Log.e(TAG, "Attempting to delete book with ID: $bookId") Log.e(TAG, "Attempting to delete book with ID: $bookId")
try { try {
// Check if book exists in Room database
val bookEntity = bookDao.getBook(bookId) val bookEntity = bookDao.getBook(bookId)
Log.e(TAG, if (bookEntity != null) { Log.e(TAG, if (bookEntity != null) {
"Found book in database: ${bookEntity.title} (ID: ${bookEntity.id})" "Found book in database: ${bookEntity.title} (ID: ${bookEntity.id})"
@@ -68,56 +55,25 @@ class BookStore(private val context: Context) {
}) })
if (bookEntity != null) { if (bookEntity != null) {
// Try to delete from Room database
try { try {
bookDao.deleteBook(bookEntity) bookDao.deleteBook(bookEntity)
Log.e(TAG, "Successfully deleted from database: ${bookEntity.title}") Log.e(TAG, "Successfully deleted book from Room database")
} catch (e: Exception) {
Log.e(TAG, "Failed to delete from database: ${e.message}", e)
}
}
// File deletion
val bookFile = File(context.filesDir, "book_$bookId.epub")
Log.e(TAG, "Book file path: ${bookFile.absolutePath}")
if (bookFile.exists()) {
Log.e(TAG, "Book file exists, size: ${bookFile.length()} bytes")
val deleted = bookFile.delete()
Log.e(TAG, "Book file deleted: $deleted")
} else {
Log.e(TAG, "Book file does not exist")
}
val coverFile = File(context.filesDir, "cover_$bookId.jpg")
Log.e(TAG, "Cover file path: ${coverFile.absolutePath}")
if (coverFile.exists()) {
Log.e(TAG, "Cover file exists, size: ${coverFile.length()} bytes")
val deleted = coverFile.delete()
Log.e(TAG, "Cover file deleted: $deleted")
} else {
Log.e(TAG, "Cover file does not exist")
}
// DataStore cleanup
Log.e(TAG, "Starting DataStore cleanup")
try {
context.dataStore.edit { preferences ->
val currentIds = preferences[bookIdsKey]?.toMutableSet() ?: mutableSetOf()
Log.e(TAG, "Current IDs in DataStore: $currentIds")
val removed = currentIds.remove(bookId)
Log.e(TAG, "ID removed from DataStore: $removed")
preferences[bookIdsKey] = currentIds
Log.e(TAG, "Updated IDs in DataStore: $currentIds")
preferences.remove(stringPreferencesKey("title_$bookId")) // Delete associated files
preferences.remove(stringPreferencesKey("position_$bookId")) val bookFile = File(bookEntity.filePath)
Log.e(TAG, "Removed related preferences for book ID: $bookId") if (bookFile.exists() && bookFile.delete()) {
Log.e(TAG, "Successfully deleted book file")
}
bookEntity.coverImagePath?.let { coverPath ->
val coverFile = File(coverPath)
if (coverFile.exists() && coverFile.delete()) {
Log.e(TAG, "Successfully deleted cover image")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error deleting book files", e)
} }
Log.e(TAG, "DataStore cleanup completed successfully")
} catch (e: Exception) {
Log.e(TAG, "Error during DataStore cleanup", e)
} }
Log.e(TAG, "🔥 DELETE BOOK COMPLETED SUCCESSFULLY 🔥") Log.e(TAG, "🔥 DELETE BOOK COMPLETED SUCCESSFULLY 🔥")
@@ -128,4 +84,9 @@ class BookStore(private val context: Context) {
e.printStackTrace() e.printStackTrace()
} }
} }
suspend fun getBook(bookId: String): EpubBook {
val bookEntity = bookDao.getBook(bookId) ?: throw IllegalArgumentException("Book not found")
return EpubReader().readEpub(FileInputStream(bookEntity.filePath))
}
} }
@@ -2,45 +2,83 @@ package inhale.rip.epook.data
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.* import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
private val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class SettingsStore(private val context: Context) { class SettingsStore(private val context: Context) {
private val fontFamilyKey = stringPreferencesKey("font_family") private object PreferencesKeys {
private val fontSizeKey = intPreferencesKey("font_size") val FONT_SIZE = floatPreferencesKey("font_size")
private val lineHeightKey = floatPreferencesKey("line_height") val LINE_HEIGHT = floatPreferencesKey("line_height")
val PADDING = floatPreferencesKey("padding")
val fontFamily: Flow<String> = context.settingsDataStore.data.map { preferences -> val FONT_FAMILY = stringPreferencesKey("font_family")
preferences[fontFamilyKey] ?: "Georgia" fun lastPositionKey(bookId: String) = intPreferencesKey("last_position_$bookId")
} }
val fontSize: Flow<Int> = context.settingsDataStore.data.map { preferences -> suspend fun getFontSize(): Float {
preferences[fontSizeKey] ?: 16 return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.FONT_SIZE] ?: 16f
}.first()
} }
val lineHeight: Flow<Float> = context.settingsDataStore.data.map { preferences -> suspend fun getLineHeight(): Float {
preferences[lineHeightKey] ?: 1.6f return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.LINE_HEIGHT] ?: 1.5f
}.first()
} }
suspend fun updateFontFamily(fontFamily: String) { suspend fun getPadding(): Float {
context.settingsDataStore.edit { preferences -> return context.dataStore.data.map { preferences ->
preferences[fontFamilyKey] = fontFamily preferences[PreferencesKeys.PADDING] ?: 16f
} }.first()
} }
suspend fun updateFontSize(size: Int) { suspend fun updateFontSize(size: Float) {
context.settingsDataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[fontSizeKey] = size preferences[PreferencesKeys.FONT_SIZE] = size
} }
} }
suspend fun updateLineHeight(height: Float) { suspend fun updateLineHeight(height: Float) {
context.settingsDataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[lineHeightKey] = height preferences[PreferencesKeys.LINE_HEIGHT] = height
} }
} }
}
suspend fun updatePadding(padding: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.PADDING] = padding
}
}
suspend fun getFontFamily(): String {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.FONT_FAMILY] ?: "Roboto"
}.first()
}
suspend fun updateFontFamily(family: String) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.FONT_FAMILY] = family
}
}
suspend fun getLastPosition(bookId: String): Int? {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.lastPositionKey(bookId)]
}.first()
}
suspend fun saveLastPosition(bookId: String, position: Int) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.lastPositionKey(bookId)] = position
}
}
}
@@ -3,40 +3,46 @@ package inhale.rip.epook.ui.theme
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme( private val LightColors = lightColorScheme(
primary = Purple80, primary = Color(0xFF2D5DA1),
secondary = PurpleGrey80, onPrimary = Color.White,
tertiary = Pink80 primaryContainer = Color(0xFFD5E3FF),
onPrimaryContainer = Color(0xFF001B3D),
secondary = Color(0xFF565E71),
onSecondary = Color.White,
secondaryContainer = Color(0xFFDAE2F9),
background = Color(0xFFFBFCFF),
surface = Color(0xFFFBFCFF),
surfaceVariant = Color(0xFFE1E2EC),
onSurfaceVariant = Color(0xFF44474F)
) )
private val LightColorScheme = lightColorScheme( private val DarkColors = darkColorScheme(
primary = Purple40, primary = Color(0xFFA6C8FF),
secondary = PurpleGrey40, onPrimary = Color(0xFF003062),
tertiary = Pink40 primaryContainer = Color(0xFF004689),
onPrimaryContainer = Color(0xFFD5E3FF),
/* Other default colors to override secondary = Color(0xFFBEC6DC),
background = Color(0xFFFFFBFE), onSecondary = Color(0xFF283041),
surface = Color(0xFFFFFBFE), secondaryContainer = Color(0xFF3E4759),
onPrimary = Color.White, background = Color(0xFF1B1B1F),
onSecondary = Color.White, surface = Color(0xFF1B1B1F),
onTertiary = Color.White, surfaceVariant = Color(0xFF44474F),
onBackground = Color(0xFF1C1B1F), onSurfaceVariant = Color(0xFFC4C6D0)
onSurface = Color(0xFF1C1B1F),
*/
) )
@Composable @Composable
fun EpookTheme( fun EpookTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
@@ -45,9 +51,17 @@ fun EpookTheme(
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
darkTheme -> DarkColors
else -> LightColors
}
darkTheme -> DarkColorScheme val view = LocalView.current
else -> LightColorScheme if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.surface.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
} }
MaterialTheme( MaterialTheme(
@@ -6,29 +6,41 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography( val Typography = Typography(
bodyLarge = TextStyle( headlineLarge = TextStyle(
fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold,
fontWeight = FontWeight.Normal, fontSize = 32.sp,
fontSize = 16.sp, lineHeight = 40.sp,
lineHeight = 24.sp, letterSpacing = 0.sp
letterSpacing = 0.5.sp ),
) headlineMedium = TextStyle(
/* Other default text styles to override fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
labelSmall = TextStyle( bodyLarge = TextStyle(
fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 11.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) )
*/
) )