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")
}
}
} }
@@ -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,42 +102,45 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
EpookTheme {
App() App()
} }
} }
} }
}
@Composable @Composable
fun App() { fun App() {
val navController = rememberNavController() val navController = rememberNavController()
val context = LocalContext.current
val settingsStore = remember(context) { SettingsStore(context) }
EpookTheme {
NavHost(navController = navController, startDestination = Screen.BookList.route) { NavHost(navController = navController, startDestination = Screen.BookList.route) {
composable(Screen.BookList.route) { composable(Screen.BookList.route) {
BookshelfScreen( BookshelfScreen(
onBookClick = { bookId -> onBookClick = { bookId ->
navController.navigate("reader/$bookId") navController.navigate(Screen.Reader.createRoute(bookId))
} }
) )
} }
composable( composable(
route = "reader/{bookId}", route = Screen.Reader.route,
arguments = listOf(navArgument("bookId") { type = NavType.StringType }) arguments = listOf(navArgument("bookId") { type = NavType.StringType })
) { backStackEntry -> ) { backStackEntry ->
val bookId = backStackEntry.arguments?.getString("bookId") ?: ""
ReaderScreen( ReaderScreen(
bookId = backStackEntry.arguments?.getString("bookId") ?: "", bookId = bookId,
onNavigateBack = { navController.navigateUp() }, onNavigateBack = { navController.navigateUp() }
onOpenSettings = { navController.navigate(Screen.Settings.route) }
) )
} }
composable(Screen.Settings.route) { composable(Screen.Settings.route) {
SettingsScreen( SettingsScreen(
onNavigateBack = { navController.navigateUp() } onNavigateBack = { navController.navigateUp() },
settingsStore = settingsStore
) )
} }
} }
} }
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
@@ -96,50 +152,33 @@ 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 bookId = System.currentTimeMillis().toString() val inputStream = context.contentResolver.openInputStream(selectedUri)
val book = EpubReader().readEpub(inputStream)
val bookId = java.util.UUID.randomUUID().toString()
val epubFile = File(context.filesDir, "book_$bookId.epub") val epubFile = File(context.filesDir, "book_$bookId.epub")
context.contentResolver.openInputStream(selectedUri)?.use { input ->
println("Saving book to: ${epubFile.absolutePath}") FileOutputStream(epubFile).use { output ->
input.copyTo(output)
// Use buffered streams for better performance
inputStream.buffered().use { bufferedInput ->
FileOutputStream(epubFile).buffered().use { bufferedOutput ->
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 -> val coverFile = book.coverImage?.let { coverImage ->
File(context.filesDir, "cover_$bookId.jpg").also { file -> File(context.filesDir, "cover_$bookId.jpg").also { file ->
FileOutputStream(file).buffered().use { output -> FileOutputStream(file).use { output ->
output.write(coverImage.data) output.write(coverImage.data)
} }
} }
} }
// Add the book to the store
scope.launch { scope.launch {
bookStore.addBook(Book( bookStore.addBook(Book(
id = bookId, id = bookId,
@@ -148,36 +187,57 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
filePath = epubFile.absolutePath filePath = epubFile.absolutePath
)) ))
} }
}
} catch (e: Exception) { } catch (e: Exception) {
println("Error saving book: ${e.message}") println("Error saving book: ${e.message}")
e.printStackTrace() 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(
onClick = {
scope.launch { scope.launch {
bookToDelete?.let { bookStore.deleteBook(it.id) } 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 ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
if (showEmptyState) {
Column(
modifier = Modifier
.fillMaxSize()
.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( LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp), columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = padding, contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
items(books) { book -> items(
Card( items = books,
modifier = Modifier key = { it.id }
.padding(8.dp) ) { book ->
.combinedClickable( 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
) { ) {
Column { 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
)
) {
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
) )
Surface(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
) {
Text( Text(
text = book.title, text = book.title,
modifier = Modifier.padding(8.dp), style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.bodyMedium modifier = Modifier.padding(12.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
} }
} }
} }
}
+316 -204
View File
@@ -2,289 +2,401 @@ 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()
// Function to load content for a specific page var showControls by remember { mutableStateOf(true) }
fun loadPage(pageIndex: Int) { var showSettings by remember { mutableStateOf(false) }
scope.launch { 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) }
// Move processChapterHtml outside the Composable
fun processChapterHtml(rawHtml: String, chapterIndex: Int): String {
val document = Jsoup.parse(rawHtml)
val body = document.body()
body.select("script, style").remove()
return "<div class='chapter' id='chapter_$chapterIndex'>\n${body.html()}\n</div>"
}
// Load book content
LaunchedEffect(bookId) {
try { try {
book?.spine?.spineReferences?.getOrNull(pageIndex)?.let { ref -> Timber.d("📚 Starting to load book with ID: $bookId")
val resource = ref.resource val book = bookStore.getBook(bookId)
val html = String(resource.data, Charsets.UTF_8) Timber.d("📚 Book loaded successfully: ${book.title}")
val doc = Jsoup.parse(html)
// Remove unnecessary elements but keep styling // Load settings first
doc.select("script").remove() 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")
// Add custom CSS for better reading experience // Process chapters
val style = doc.head().appendElement("style") val processedChapters = mutableListOf<String>()
style.appendText(""" 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}")
val processedHtml = processChapterHtml(rawHtml, index)
processedChapters.add(processedHtml)
Timber.d("📚 Chapter $index processed successfully")
}
}
// 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")
}
}
val css = """
:root {
--page-width: calc(100vw - ${padding * 2}px);
}
body { body {
font-family: '$fontFamily', serif; margin: 0;
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; font-size: ${fontSize}px;
line-height: $lineHeight; line-height: ${lineHeight}em;
padding: 20px; background-color: ${if (isDarkMode) "#1C1B1F" else "#FFFFFF"};
max-width: 800px; color: ${if (isDarkMode) "#E6E1E5" else "#1C1B1F"};
margin: 0 auto;
color: #333;
background-color: #f8f8f8;
} }
p { .chapter {
margin: 1em 0; break-inside: avoid;
text-align: justify; margin-bottom: 2em;
} display: inline-block;
h1, h2, h3, h4, h5, h6 { width: 100%;
color: #2c3e50;
margin: 1.5em 0 0.5em;
} }
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
display: block;
margin: 1em auto;
} }
@media (prefers-color-scheme: dark) { p {
body { margin: 0.5em 0;
background-color: #1a1a1a; text-align: justify;
color: #e0e0e0;
} }
h1, h2, h3, h4, h5, h6 { """.trimIndent()
color: #b0b0b0;
}
}
""".trimIndent())
currentHtml = doc.outerHtml() BackHandler(enabled = showSettings || showChapterList) {
println("Loaded page $pageIndex") when {
showSettings -> showSettings = false
// Save the position whenever we load a new page showChapterList -> showChapterList = false
bookStore.updateReadingPosition(bookId, pageIndex)
} ?: run {
errorMessage = "Page not found"
}
} catch (e: Exception) {
println("Error loading page: ${e.message}")
errorMessage = "Error loading page: ${e.message}"
}
}
}
LaunchedEffect(bookId) {
scope.launch {
try {
val bookFile = File(context.filesDir, "book_$bookId.epub")
if (!bookFile.exists()) {
errorMessage = "Book file not found"
return@launch
}
bookFile.inputStream().buffered().use { inputStream ->
val epubReader = EpubReader()
book = epubReader.readEpub(inputStream)
totalPages = book?.spine?.spineReferences?.size ?: 0
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 = {
AnimatedVisibility(
visible = showControls,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
TopAppBar( TopAppBar(
title = { Text(book?.title ?: "Loading...") }, title = {
Column {
Text("Chapter ${currentChapter + 1}")
Text(
"Page $currentPage of $totalPages",
style = MaterialTheme.typography.bodyMedium
)
}
},
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
} }
}, },
actions = { actions = {
IconButton(onClick = onOpenSettings) { IconButton(onClick = { showChapterList = true }) {
Icon(Icons.Default.Settings, contentDescription = "Settings") 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"
)
} }
} }
) )
} }
}
) { padding -> ) { padding ->
Box( Box(modifier = Modifier.padding(padding)) {
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
when {
errorMessage != null -> {
Column(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = errorMessage ?: "Unknown error",
color = MaterialTheme.colorScheme.error
)
}
}
currentHtml == null -> {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
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)
}
}
}
}
) {
// WebView for rendering HTML content
AndroidView( AndroidView(
factory = { context -> factory = { context ->
WebView(context).apply { WebView(context).apply {
settings.apply { settings.apply {
javaScriptEnabled = false javaScriptEnabled = true
builtInZoomControls = true
displayZoomControls = false
useWideViewPort = true useWideViewPort = true
loadWithOverviewMode = true loadWithOverviewMode = true
defaultFontSize = fontSize.toInt()
Timber.d("📚 WebView settings configured")
} }
webViewClient = WebViewClient()
setBackgroundColor(android.graphics.Color.TRANSPARENT) webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
super.onPageStarted(view, url, favicon)
Timber.d("📚 WebView page load started")
} }
},
modifier = Modifier.weight(1f), override fun onPageFinished(view: WebView?, url: String?) {
update = { webView -> super.onPageFinished(view, url)
webView.loadDataWithBaseURL( Timber.d("📚 WebView page load finished")
// Add a slight delay to ensure content is fully laid out
view?.postDelayed({
Timber.d("📚 Starting page calculation")
view.evaluateJavascript(PAGE_CALCULATION_JS) { result ->
try {
val pages = result.toInt()
Timber.d("📚 Page calculation complete - Total pages: $pages")
totalPages = pages
} catch (e: Exception) {
Timber.e(e, "📚 Error calculating pages")
}
}
// 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, null,
currentHtml ?: "", """
<!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", "text/html",
"UTF-8", "UTF-8",
null 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
}
}
}
} }
) )
// Navigation controls // Settings sheet
Row( if (showSettings) {
ModalBottomSheet(
onDismissRequest = { showSettings = false }
) {
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(16.dp)
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton( Text("Reading Settings", style = MaterialTheme.typography.titleLarge)
onClick = { Spacer(modifier = Modifier.height(16.dp))
if (currentPageIndex > 0) {
currentPageIndex--
loadPage(currentPageIndex)
}
},
enabled = currentPageIndex > 0
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Previous page"
)
}
Text( Text("Font Size: $fontSize", style = MaterialTheme.typography.bodyMedium)
text = "Page ${currentPageIndex + 1} of $totalPages", Slider(
style = MaterialTheme.typography.bodySmall value = fontSize,
onValueChange = {
fontSize = it
// Update WebView font size
webViewRef?.evaluateJavascript(
"document.body.style.fontSize = '${fontSize}px'",
null
)
},
valueRange = 12f..24f,
steps = 11
) )
IconButton( // Similar sliders for lineHeight, padding, brightness
onClick = { // Font family selector
if (currentPageIndex < totalPages - 1) { // Color theme selector
currentPageIndex++
loadPage(currentPageIndex)
} }
}, }
enabled = currentPageIndex < totalPages - 1 }
// Chapter list
if (showChapterList) {
ModalBottomSheet(
onDismissRequest = { showChapterList = false }
) { ) {
Icon( LazyColumn(
imageVector = Icons.Default.ArrowBack, modifier = Modifier.fillMaxWidth()
contentDescription = "Next page", ) {
modifier = Modifier.rotate(180f) items(chapters) { chapter ->
ListItem(
headlineContent = { Text(chapter) },
modifier = Modifier.clickable {
// Navigate to chapter
webViewRef?.evaluateJavascript(
"document.getElementById('chapter_$currentChapter').scrollIntoView()",
null
)
showChapterList = false
}
) )
} }
} }
} }
} }
// 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,58 +55,27 @@ 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")
// Delete associated files
val bookFile = File(bookEntity.filePath)
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) { } catch (e: Exception) {
Log.e(TAG, "Failed to delete from database: ${e.message}", e) Log.e(TAG, "Error deleting book files", 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"))
preferences.remove(stringPreferencesKey("position_$bookId"))
Log.e(TAG, "Removed related preferences for book ID: $bookId")
}
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 🔥")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "🔥 DELETE BOOK FAILED 🔥") Log.e(TAG, "🔥 DELETE BOOK FAILED 🔥")
@@ -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
) )
*/
) )