book cover fixed
This commit is contained in:
@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
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.MenuBook
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -41,6 +42,11 @@ import androidx.compose.foundation.gestures.detectTapGestures
|
|||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -53,7 +59,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
EpookTheme {
|
EpookTheme {
|
||||||
NavHost(
|
NavHost(
|
||||||
@@ -64,8 +69,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
MainScreen(
|
MainScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
bookStore = bookStore,
|
bookStore = bookStore,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState
|
||||||
scope = scope
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,16 +94,18 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainScreen(
|
fun MainScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
bookStore: BookStore,
|
bookStore: BookStore,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
scope: CoroutineScope
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val books by bookStore.getAllBooks().collectAsState(initial = emptyList())
|
val books by bookStore.getAllBooks().collectAsState(initial = emptyList())
|
||||||
|
var bookToDelete by remember { mutableStateOf<Book?>(null) }
|
||||||
|
|
||||||
fun importBook(uri: Uri, scope: CoroutineScope) {
|
fun importBook(uri: Uri) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
@@ -134,15 +140,45 @@ private fun MainScreen(
|
|||||||
contract = ActivityResultContracts.GetContent()
|
contract = ActivityResultContracts.GetContent()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
importBook(uri, scope)
|
importBook(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete confirmation dialog
|
||||||
|
bookToDelete?.let { book ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { bookToDelete = null },
|
||||||
|
title = { Text("Delete Book") },
|
||||||
|
text = {
|
||||||
|
Text("Are you sure you want to delete \"${book.title}\"?")
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
bookStore.deleteBook(book.id)
|
||||||
|
bookToDelete = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Delete", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { bookToDelete = null }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { launcher.launch("application/epub+zip") }
|
onClick = { launcher.launch("application/epub+zip") },
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Add,
|
imageVector = Icons.Default.Add,
|
||||||
@@ -151,24 +187,60 @@ private fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
if (books.isEmpty()) {
|
||||||
|
EmptyLibraryMessage(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 160.dp),
|
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||||
contentPadding = padding,
|
contentPadding = PaddingValues(12.dp),
|
||||||
modifier = Modifier.fillMaxSize()
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
items(books) { book ->
|
items(books) { book ->
|
||||||
BookCard(
|
BookCard(
|
||||||
book = book,
|
book = book,
|
||||||
onClick = { navController.navigate("reader/${book.id}") },
|
onClick = { navController.navigate("reader/${book.id}") },
|
||||||
onLongClick = {
|
onLongClick = { bookToDelete = book }
|
||||||
scope.launch {
|
|
||||||
bookStore.deleteBook(book.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyLibraryMessage(modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MenuBook,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "Your library is empty",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Tap + to add your first book",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -185,7 +257,7 @@ private fun BookCard(
|
|||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(0.7f)
|
.aspectRatio(0.75f)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = { onClick() },
|
onTap = { onClick() },
|
||||||
@@ -193,42 +265,45 @@ private fun BookCard(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
elevation = CardDefaults.elevatedCardElevation(
|
elevation = CardDefaults.elevatedCardElevation(
|
||||||
defaultElevation = 4.dp,
|
defaultElevation = 2.dp,
|
||||||
pressedElevation = if (isPressed) 8.dp else 4.dp
|
pressedElevation = if (isPressed) 8.dp else 2.dp
|
||||||
|
),
|
||||||
|
colors = CardDefaults.elevatedCardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// Book cover
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = book.coverPath ?: R.drawable.ic_launcher_background,
|
model = book.coverPath ?: R.drawable.ic_book_24dp,
|
||||||
contentDescription = book.title,
|
contentDescription = book.title,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
|
|
||||||
Surface(
|
// Simple overlay for title
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter),
|
.background(
|
||||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
|
Brush.verticalGradient(
|
||||||
) {
|
colors = listOf(
|
||||||
Column(
|
Color.Transparent,
|
||||||
modifier = Modifier.padding(12.dp)
|
Color.Black.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = book.title,
|
text = book.title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = book.author,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
color = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -223,6 +223,27 @@ class BookStore(private val context: Context) {
|
|||||||
// Extract EPUB contents
|
// Extract EPUB contents
|
||||||
val extractedDirectory = File(bookDirectory, "extracted")
|
val extractedDirectory = File(bookDirectory, "extracted")
|
||||||
extractedDirectory.mkdirs()
|
extractedDirectory.mkdirs()
|
||||||
|
|
||||||
|
// Extract and save cover image
|
||||||
|
var coverPath: String? = null
|
||||||
|
|
||||||
|
// Try to get cover from the book's resources
|
||||||
|
epubBook.resources.all
|
||||||
|
.firstOrNull { resource ->
|
||||||
|
resource.mediaType?.toString()?.contains("image") == true &&
|
||||||
|
(resource.href.contains("cover") || resource.id?.contains("cover") == true)
|
||||||
|
}?.let { coverResource ->
|
||||||
|
val coverFile = File(bookDirectory, "cover.jpg")
|
||||||
|
coverFile.writeBytes(coverResource.data)
|
||||||
|
coverPath = "file://${coverFile.absolutePath}"
|
||||||
|
Timber.d("Saved cover image from resources to: $coverPath")
|
||||||
|
} ?: epubBook.coverImage?.let { cover ->
|
||||||
|
val coverFile = File(bookDirectory, "cover.jpg")
|
||||||
|
coverFile.writeBytes(cover.data)
|
||||||
|
coverPath = "file://${coverFile.absolutePath}"
|
||||||
|
Timber.d("Saved direct cover image to: $coverPath")
|
||||||
|
}
|
||||||
|
|
||||||
extractEpub(epubBook, extractedDirectory)
|
extractEpub(epubBook, extractedDirectory)
|
||||||
|
|
||||||
// Save book metadata
|
// Save book metadata
|
||||||
@@ -232,7 +253,7 @@ class BookStore(private val context: Context) {
|
|||||||
author = epubBook.metadata.authors.firstOrNull()?.toString() ?: "Unknown",
|
author = epubBook.metadata.authors.firstOrNull()?.toString() ?: "Unknown",
|
||||||
path = epubFile.absolutePath,
|
path = epubFile.absolutePath,
|
||||||
extractedPath = extractedDirectory.absolutePath,
|
extractedPath = extractedDirectory.absolutePath,
|
||||||
coverPath = null // Handle cover separately if needed
|
coverPath = coverPath
|
||||||
)
|
)
|
||||||
|
|
||||||
bookDao.insert(book.toEntity())
|
bookDao.insert(book.toEntity())
|
||||||
|
|||||||
Reference in New Issue
Block a user