nice ui and ux ig
This commit is contained in:
+39
-1
@@ -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,41 +102,44 @@ 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)
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user