first official release
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
# 📚 Epook - Modern EPUB Reader
|
||||||
|
|
||||||
|
Epook is a sleek, modern EPUB reader built for Android using Jetpack Compose. It provides a clean, intuitive interface for reading your favorite ebooks with powerful features and smooth animations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 📖 Reading Experience
|
||||||
|
- Fluid chapter navigation with swipe gestures
|
||||||
|
- Progress tracking across reading sessions
|
||||||
|
- Interactive chapter selection through table of contents
|
||||||
|
- Smooth animations for UI elements
|
||||||
|
- Full-screen reading mode with tap controls
|
||||||
|
|
||||||
|
### 📱 User Interface
|
||||||
|
- Material Design 3 with dynamic theming
|
||||||
|
- Animated controls overlay
|
||||||
|
- Bottom navigation bar with chapter progress
|
||||||
|
- Clean, minimalist book library view
|
||||||
|
- Beautiful book cards with cover display
|
||||||
|
|
||||||
|
### ⚙️ Customization
|
||||||
|
- Adjustable font size (12-24sp)
|
||||||
|
- Customizable line height (1.0x-2.0x)
|
||||||
|
- Margin control (8-32dp)
|
||||||
|
- Multiple font family options:
|
||||||
|
- Georgia
|
||||||
|
- Roboto
|
||||||
|
- Times New Roman
|
||||||
|
- Arial
|
||||||
|
- Verdana
|
||||||
|
|
||||||
|
### 📚 Library Management
|
||||||
|
- Import EPUB files
|
||||||
|
- Automatic cover image extraction
|
||||||
|
- Book deletion with confirmation
|
||||||
|
- Reading progress persistence
|
||||||
|
- Organized book collection view
|
||||||
|
|
||||||
|
### 🔧 Technical Features
|
||||||
|
- CSS stylesheet handling
|
||||||
|
- HTML content processing
|
||||||
|
- Efficient file management
|
||||||
|
- WebView-based rendering
|
||||||
|
- Resource caching
|
||||||
|
|
||||||
|
## 🛠️ Technical Stack
|
||||||
|
|
||||||
|
- **UI Framework**: Jetpack Compose
|
||||||
|
- **Language**: Kotlin
|
||||||
|
- **Architecture**: MVVM
|
||||||
|
- **Storage**: Room Database
|
||||||
|
- **EPUB Processing**: epublib
|
||||||
|
- **HTML Processing**: JSoup
|
||||||
|
- **Image Loading**: Coil
|
||||||
|
- **Logging**: Timber
|
||||||
|
|
||||||
|
## 🎯 Upcoming Features
|
||||||
|
|
||||||
|
- [ ] Search functionality
|
||||||
|
- [ ] Bookmarks
|
||||||
|
- [ ] Highlights and notes
|
||||||
|
- [ ] Dark mode support
|
||||||
|
- [ ] Reading statistics
|
||||||
|
- [ ] Custom themes
|
||||||
|
- [ ] Cloud sync
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Feel free to submit issues and pull requests.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- [epublib](https://github.com/psiegman/epublib) for EPUB processing
|
||||||
|
- [Jsoup](https://jsoup.org/) for HTML parsing
|
||||||
|
- [Material Design 3](https://m3.material.io/) for design guidelines
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -173,6 +178,26 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
SmallTopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "My Library",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${books.size} book${if (books.size != 1) "s" else ""}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
@@ -195,10 +220,10 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 120.dp),
|
columns = GridCells.Adaptive(minSize = 140.dp),
|
||||||
contentPadding = PaddingValues(12.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
@@ -254,22 +279,27 @@ private fun BookCard(
|
|||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier.width(140.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(0.75f)
|
.aspectRatio(0.67f) // Standard book cover ratio
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = { onClick() },
|
onTap = { onClick() },
|
||||||
onLongPress = { onLongClick() }
|
onLongPress = { onLongClick() }
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
elevation = CardDefaults.elevatedCardElevation(
|
.shadow(
|
||||||
defaultElevation = 2.dp,
|
elevation = if (isPressed) 8.dp else 4.dp,
|
||||||
pressedElevation = if (isPressed) 8.dp else 2.dp
|
shape = RoundedCornerShape(8.dp)
|
||||||
),
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
colors = CardDefaults.elevatedCardColors(
|
colors = CardDefaults.elevatedCardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
@@ -281,29 +311,41 @@ private fun BookCard(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Simple overlay for title
|
// Gradient overlay
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.height(80.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
brush = Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Transparent,
|
Color.Transparent,
|
||||||
Color.Black.copy(alpha = 0.6f)
|
Color.Black.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.align(Alignment.BottomCenter)
|
)
|
||||||
.padding(8.dp)
|
}
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
// Book info below the card
|
||||||
Text(
|
Text(
|
||||||
text = book.title,
|
text = book.title,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.White
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = book.author,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.animation.*
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -77,9 +78,10 @@ fun ReaderScreen(
|
|||||||
var showChapterList by remember { mutableStateOf(false) }
|
var showChapterList by remember { mutableStateOf(false) }
|
||||||
var showSettings by remember { mutableStateOf(false) }
|
var showSettings by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
|
var currentX by remember { mutableStateOf(0f) }
|
||||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
|
||||||
val primaryColor = MaterialTheme.colorScheme.primary.toArgb()
|
var extractedPath by remember { mutableStateOf<String?>(null) }
|
||||||
|
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
@@ -147,63 +149,46 @@ fun ReaderScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
topBar = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = showControls,
|
|
||||||
enter = fadeIn() + slideInVertically(),
|
|
||||||
exit = fadeOut() + slideOutVertically()
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = book?.title ?: "Reader",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Chapter ${currentChapterIndex + 1} of ${chapters.size}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onNavigateBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { showChapterList = true }) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "Chapters")
|
|
||||||
}
|
|
||||||
IconButton(onClick = { showSettings = true }) {
|
|
||||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(bottom = if (showControls) 80.dp else 0.dp)
|
||||||
) {
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
AndroidView(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = { showControls = !showControls }
|
onTap = { showControls = !showControls }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
var initialX = 0f
|
||||||
|
detectHorizontalDragGestures(
|
||||||
|
onDragStart = { offset ->
|
||||||
|
initialX = offset.x
|
||||||
},
|
},
|
||||||
factory = { context ->
|
onDragEnd = {
|
||||||
var extractedPath: String? = null
|
// Implement drag threshold for swipe
|
||||||
|
val dragThreshold = size.width * 0.2f // 20% of screen width
|
||||||
|
val dragDistance = initialX - currentX
|
||||||
|
|
||||||
|
when {
|
||||||
|
dragDistance > dragThreshold && currentChapterIndex < chapters.size - 1 -> {
|
||||||
|
// Swiped left - next chapter
|
||||||
|
currentChapterIndex++
|
||||||
|
}
|
||||||
|
dragDistance < -dragThreshold && currentChapterIndex > 0 -> {
|
||||||
|
// Swiped right - previous chapter
|
||||||
|
currentChapterIndex--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { change, _ ->
|
||||||
|
currentX = change.position.x
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
WebView(context).apply {
|
WebView(context).apply {
|
||||||
webViewClient = object : WebViewClient() {
|
webViewClient = object : WebViewClient() {
|
||||||
override fun onReceivedError(
|
override fun onReceivedError(
|
||||||
@@ -222,18 +207,16 @@ fun ReaderScreen(
|
|||||||
val url = request.url.toString()
|
val url = request.url.toString()
|
||||||
Timber.d("Loading resource: $url")
|
Timber.d("Loading resource: $url")
|
||||||
|
|
||||||
// Try to load the resource from the extracted directory
|
|
||||||
try {
|
try {
|
||||||
if (url.endsWith(".css") && extractedPath != null) {
|
if (url.endsWith(".css") && extractedPath != null) {
|
||||||
// Try multiple possible CSS file locations
|
|
||||||
val possiblePaths = listOf(
|
val possiblePaths = listOf(
|
||||||
url.substringAfterLast("/"), // stylesheet.css
|
url.substringAfterLast("/"),
|
||||||
"Styles/${url.substringAfterLast("/")}", // Styles/stylesheet.css
|
"Styles/${url.substringAfterLast("/")}",
|
||||||
url.substringAfter("/extracted/") // Text/Styles/stylesheet.css
|
url.substringAfter("/extracted/")
|
||||||
)
|
)
|
||||||
|
|
||||||
for (cssPath in possiblePaths) {
|
for (cssPath in possiblePaths) {
|
||||||
val cssFile = File(File(extractedPath!!), cssPath)
|
val cssFile = File(extractedPath!!, cssPath)
|
||||||
if (cssFile.exists()) {
|
if (cssFile.exists()) {
|
||||||
Timber.d("Found CSS file at: ${cssFile.absolutePath}")
|
Timber.d("Found CSS file at: ${cssFile.absolutePath}")
|
||||||
return WebResourceResponse(
|
return WebResourceResponse(
|
||||||
@@ -243,13 +226,10 @@ fun ReaderScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.e("CSS file not found. Tried paths: ${possiblePaths.joinToString()}")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Error intercepting resource request")
|
Timber.e(e, "Error intercepting resource request")
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
return super.shouldInterceptRequest(view, request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,33 +239,22 @@ fun ReaderScreen(
|
|||||||
builtInZoomControls = true
|
builtInZoomControls = true
|
||||||
displayZoomControls = false
|
displayZoomControls = false
|
||||||
allowFileAccess = true
|
allowFileAccess = true
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
allowFileAccessFromFileURLs = true
|
allowFileAccessFromFileURLs = true
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
allowUniversalAccessFromFileURLs = true
|
allowUniversalAccessFromFileURLs = true
|
||||||
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||||
domStorageEnabled = true
|
domStorageEnabled = true
|
||||||
|
|
||||||
// Remove deprecated calls
|
|
||||||
// setAppCacheEnabled and setAppCachePath are deprecated
|
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
}
|
}
|
||||||
}.also { webView ->
|
}.also { webView = it }
|
||||||
// Update extractedPath when loading URLs
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
extractedPath = bookStore.getBook(bookId)?.extractedPath
|
|
||||||
?: throw IllegalStateException("Book not properly extracted")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error getting extracted path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
update = { webView ->
|
update = { view ->
|
||||||
val chapter = chapters.getOrNull(currentChapterIndex)?.resource
|
val chapter = chapters.getOrNull(currentChapterIndex)?.resource
|
||||||
chapter?.let { resource ->
|
chapter?.let { resource ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val extractedPath = bookStore.getBook(bookId)?.extractedPath
|
extractedPath = bookStore.getBook(bookId)?.extractedPath
|
||||||
?: throw IllegalStateException("Book not properly extracted")
|
?: throw IllegalStateException("Book not properly extracted")
|
||||||
|
|
||||||
val baseUrl = "file://$extractedPath/"
|
val baseUrl = "file://$extractedPath/"
|
||||||
@@ -294,87 +263,106 @@ fun ReaderScreen(
|
|||||||
Timber.d("Loading chapter from: $fullUrl")
|
Timber.d("Loading chapter from: $fullUrl")
|
||||||
Timber.d("Base URL: $baseUrl")
|
Timber.d("Base URL: $baseUrl")
|
||||||
|
|
||||||
webView.loadUrl(fullUrl)
|
view.loadUrl(fullUrl)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Error loading chapter")
|
Timber.e(e, "Error loading chapter")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation controls
|
// Controls overlay
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showControls,
|
visible = showControls,
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
enter = slideInVertically(initialOffsetY = { it }),
|
||||||
enter = fadeIn() + slideInVertically { it },
|
exit = slideOutVertically(targetOffsetY = { it }),
|
||||||
exit = fadeOut() + slideOutVertically { it }
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
|
||||||
|
tonalElevation = 8.dp,
|
||||||
|
shadowElevation = 8.dp
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// Chapter progress
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { currentChapterIndex.toFloat() / (chapters.size - 1).coerceAtLeast(1).toFloat() },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
|
|
||||||
tonalElevation = 3.dp,
|
|
||||||
shape = MaterialTheme.shapes.large
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Previous Chapter Button
|
// Previous chapter
|
||||||
FilledTonalButton(
|
IconButton(
|
||||||
onClick = { if (currentChapterIndex > 0) currentChapterIndex-- },
|
onClick = { if (currentChapterIndex > 0) currentChapterIndex-- },
|
||||||
enabled = currentChapterIndex > 0,
|
enabled = currentChapterIndex > 0
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.NavigateBefore,
|
imageVector = Icons.AutoMirrored.Filled.NavigateBefore,
|
||||||
contentDescription = null,
|
contentDescription = "Previous Chapter"
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
Text("Back")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapter Counter
|
// Chapter selector
|
||||||
|
TextButton(
|
||||||
|
onClick = { showChapterList = true }
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${currentChapterIndex + 1} / ${chapters.size}",
|
text = "Chapter ${currentChapterIndex + 1}/${chapters.size}",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.bodyMedium
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Next Chapter Button
|
// Next chapter
|
||||||
FilledTonalButton(
|
IconButton(
|
||||||
onClick = { if (currentChapterIndex < chapters.size - 1) currentChapterIndex++ },
|
onClick = { if (currentChapterIndex < chapters.size - 1) currentChapterIndex++ },
|
||||||
enabled = currentChapterIndex < chapters.size - 1,
|
enabled = currentChapterIndex < chapters.size - 1
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("Next")
|
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.NavigateNext,
|
imageVector = Icons.AutoMirrored.Filled.NavigateNext,
|
||||||
contentDescription = null,
|
contentDescription = "Next Chapter"
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Back button overlay
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showControls,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onNavigateBack,
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user