nice ui and ux ig

This commit is contained in:
inhale-dir 2024-12-13 11:49:43 +01:00
parent e57d8b7490
commit 04820240dc
8 changed files with 859 additions and 515 deletions

View File

@ -79,7 +79,6 @@ dependencies {
exclude(group = "xmlpull", module = "xmlpull")
}
implementation("org.slf4j:slf4j-android:1.7.36")
implementation("org.jsoup:jsoup:1.15.3")
implementation("net.sf.kxml:kxml2:2.3.0")
// Add material icons extended for the settings icon
@ -93,4 +92,43 @@ dependencies {
// Add Timber for logging
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")
}
}
}

View File

@ -15,6 +15,8 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
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.runtime.*
import androidx.compose.ui.Modifier
@ -35,13 +37,64 @@ import kotlinx.coroutines.launch
import androidx.navigation.NavType
import androidx.navigation.navArgument
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) {
object BookList : Screen("books")
object Reader : Screen("reader/{bookId}")
object BookList : Screen("bookList")
object Settings : Screen("settings")
object Reader : Screen("reader/{bookId}") {
fun createRoute(bookId: String) = "reader/$bookId"
}
}
class MainActivity : ComponentActivity() {
@ -49,9 +102,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
EpookTheme {
App()
}
App()
}
}
}
@ -59,29 +110,34 @@ class MainActivity : ComponentActivity() {
@Composable
fun App() {
val navController = rememberNavController()
val context = LocalContext.current
val settingsStore = remember(context) { SettingsStore(context) }
NavHost(navController = navController, startDestination = Screen.BookList.route) {
composable(Screen.BookList.route) {
BookshelfScreen(
onBookClick = { bookId ->
navController.navigate("reader/$bookId")
}
)
}
composable(
route = "reader/{bookId}",
arguments = listOf(navArgument("bookId") { type = NavType.StringType })
) { backStackEntry ->
ReaderScreen(
bookId = backStackEntry.arguments?.getString("bookId") ?: "",
onNavigateBack = { navController.navigateUp() },
onOpenSettings = { navController.navigate(Screen.Settings.route) }
)
}
composable(Screen.Settings.route) {
SettingsScreen(
onNavigateBack = { navController.navigateUp() }
)
EpookTheme {
NavHost(navController = navController, startDestination = Screen.BookList.route) {
composable(Screen.BookList.route) {
BookshelfScreen(
onBookClick = { bookId ->
navController.navigate(Screen.Reader.createRoute(bookId))
}
)
}
composable(
route = Screen.Reader.route,
arguments = listOf(navArgument("bookId") { type = NavType.StringType })
) { backStackEntry ->
val bookId = backStackEntry.arguments?.getString("bookId") ?: ""
ReaderScreen(
bookId = bookId,
onNavigateBack = { navController.navigateUp() }
)
}
composable(Screen.Settings.route) {
SettingsScreen(
onNavigateBack = { navController.navigateUp() },
settingsStore = settingsStore
)
}
}
}
}
@ -96,88 +152,92 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
var showDeleteDialog by remember { mutableStateOf(false) }
var bookToDelete by remember { mutableStateOf<Book?>(null) }
var showEmptyState by remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let { selectedUri ->
context.contentResolver.openInputStream(selectedUri)?.use { inputStream ->
try {
val bookId = System.currentTimeMillis().toString()
val epubFile = File(context.filesDir, "book_$bookId.epub")
println("Saving book to: ${epubFile.absolutePath}")
// Use buffered streams for better performance
inputStream.buffered().use { bufferedInput ->
FileOutputStream(epubFile).buffered().use { bufferedOutput ->
bufferedInput.copyTo(bufferedOutput)
}
try {
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")
context.contentResolver.openInputStream(selectedUri)?.use { input ->
FileOutputStream(epubFile).use { output ->
input.copyTo(output)
}
println("Book file saved, size: ${epubFile.length()} bytes")
// Verify the file exists and is readable
if (!epubFile.exists() || !epubFile.canRead()) {
println("Error: File not accessible after saving")
return@use
}
// Test reading the EPUB
epubFile.inputStream().buffered().use { fileInput ->
val epubReader = EpubReader()
val book = epubReader.readEpub(fileInput)
println("Book loaded for verification: ${book.title}")
println("Number of spine refs: ${book.spine.spineReferences.size}")
// Extract and save cover image if it exists
val coverFile = book.coverImage?.let { coverImage ->
File(context.filesDir, "cover_$bookId.jpg").also { file ->
FileOutputStream(file).buffered().use { output ->
output.write(coverImage.data)
}
}
}
// Add the book to the store
scope.launch {
bookStore.addBook(Book(
id = bookId,
title = book.title ?: "Unknown Title",
coverImageFile = coverFile,
filePath = epubFile.absolutePath
))
}
}
} catch (e: Exception) {
println("Error saving book: ${e.message}")
e.printStackTrace()
}
val coverFile = book.coverImage?.let { coverImage ->
File(context.filesDir, "cover_$bookId.jpg").also { file ->
FileOutputStream(file).use { output ->
output.write(coverImage.data)
}
}
}
scope.launch {
bookStore.addBook(Book(
id = bookId,
title = book.title ?: "Unknown Title",
coverImageFile = coverFile,
filePath = epubFile.absolutePath
))
}
} catch (e: Exception) {
println("Error saving book: ${e.message}")
e.printStackTrace()
}
}
}
LaunchedEffect(books) {
showEmptyState = books.isEmpty()
}
if (showDeleteDialog && bookToDelete != null) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Delete Book") },
text = { Text("Are you sure you want to delete '${bookToDelete?.title}'?") },
icon = {
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 = {
TextButton(onClick = {
scope.launch {
bookToDelete?.let { bookStore.deleteBook(it.id) }
}
showDeleteDialog = false
bookToDelete = null
}) {
Button(
onClick = {
scope.launch {
bookToDelete?.let { bookStore.deleteBook(it.id) }
showDeleteDialog = false
bookToDelete = null
}
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = {
showDeleteDialog = false
bookToDelete = null
}) {
OutlinedButton(onClick = { showDeleteDialog = false }) {
Text("Cancel")
}
}
@ -185,42 +245,97 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
}
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(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")
}
}
) { padding ->
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
contentPadding = padding,
modifier = Modifier.fillMaxSize()
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(books) { book ->
Card(
if (showEmptyState) {
Column(
modifier = Modifier
.padding(8.dp)
.combinedClickable(
.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(
columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize()
) {
items(
items = books,
key = { it.id }
) { book ->
BookCard(
book = book,
onClick = { onBookClick(book.id) },
onLongClick = {
bookToDelete = book
showDeleteDialog = true
}
)
) {
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
},
modifier = Modifier.animateItemPlacement()
)
}
}
@ -229,27 +344,54 @@ fun BookshelfScreen(onBookClick: (String) -> Unit) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookCover(book: Book, onClick: () -> Unit) {
Card(
modifier = Modifier
.padding(8.dp)
.clickable(onClick = onClick)
private fun BookCard(
book: Book,
onClick: () -> Unit,
onLongClick: () -> Unit,
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(
model = book.coverImageFile ?: R.drawable.ic_launcher_background,
contentDescription = book.title,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.7f),
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Text(
text = book.title,
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.bodyMedium
)
Surface(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
) {
Text(
text = book.title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(12.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}

View File

@ -2,290 +2,402 @@ package inhale.rip.epook
import android.webkit.WebView
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
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 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)
@Composable
fun ReaderScreen(
bookId: String,
onNavigateBack: () -> Unit,
onOpenSettings: () -> Unit
onNavigateBack: () -> 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 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 savedPosition by bookStore.getReadingPosition(bookId).collectAsState(initial = 0)
val settingsStore = remember { SettingsStore(context) }
val scope = rememberCoroutineScope()
var showControls by remember { mutableStateOf(true) }
var showSettings by remember { mutableStateOf(false) }
var brightness by remember { mutableStateOf(1f) }
var isDarkMode by remember { mutableStateOf(false) }
var fontSize by remember { mutableStateOf(16f) }
var lineHeight by remember { mutableStateOf(1.5f) }
var padding by remember { mutableStateOf(16f) }
var fontFamily by remember { mutableStateOf("Roboto") }
var currentChapter by remember { mutableStateOf(0) }
var showChapterList by remember { mutableStateOf(false) }
var chapters by remember { mutableStateOf(listOf<String>()) }
var currentPage by remember { mutableStateOf(1) }
var totalPages by remember { mutableStateOf(1) }
var bookContent by remember { mutableStateOf("") }
var webViewRef by remember { mutableStateOf<WebView?>(null) }
// Function to load content for a specific page
fun loadPage(pageIndex: Int) {
scope.launch {
try {
book?.spine?.spineReferences?.getOrNull(pageIndex)?.let { ref ->
val resource = ref.resource
val html = String(resource.data, Charsets.UTF_8)
val doc = Jsoup.parse(html)
// 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 {
Timber.d("📚 Starting to load book with ID: $bookId")
val book = bookStore.getBook(bookId)
Timber.d("📚 Book loaded successfully: ${book.title}")
// Load settings first
val savedSettings = settingsStore.loadSettings()
currentPage = savedSettings.currentPage
fontSize = savedSettings.fontSize
lineHeight = savedSettings.lineHeight
padding = savedSettings.padding
fontFamily = savedSettings.fontFamily
isDarkMode = savedSettings.isDarkMode
Timber.d("📚 Settings loaded - Page: $currentPage, Font: $fontSize, Line: $lineHeight, Padding: $padding")
// Process chapters
val processedChapters = mutableListOf<String>()
book.spine.spineReferences.forEachIndexed { index, spineReference ->
Timber.d("📚 Processing chapter $index")
val resource = book.resources.getByHref(spineReference.resource.href)
Timber.d("📚 Resource href: ${resource.href}")
Timber.d("📚 Resource media type: ${resource.mediaType}")
if (resource.mediaType == MediaType.XHTML) {
val rawHtml = String(resource.data, Charset.forName("UTF-8"))
Timber.d("📚 Raw HTML length: ${rawHtml.length}")
// Remove unnecessary elements but keep styling
doc.select("script").remove()
// Add custom CSS for better reading experience
val style = doc.head().appendElement("style")
style.appendText("""
body {
font-family: '$fontFamily', serif;
font-size: ${fontSize}px;
line-height: $lineHeight;
padding: 20px;
max-width: 800px;
margin: 0 auto;
color: #333;
background-color: #f8f8f8;
}
p {
margin: 1em 0;
text-align: justify;
}
h1, h2, h3, h4, h5, h6 {
color: #2c3e50;
margin: 1.5em 0 0.5em;
}
img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #e0e0e0;
}
h1, h2, h3, h4, h5, h6 {
color: #b0b0b0;
}
}
""".trimIndent())
currentHtml = doc.outerHtml()
println("Loaded page $pageIndex")
// Save the position whenever we load a new page
bookStore.updateReadingPosition(bookId, pageIndex)
} ?: run {
errorMessage = "Page not found"
val processedHtml = processChapterHtml(rawHtml, index)
processedChapters.add(processedHtml)
Timber.d("📚 Chapter $index processed successfully")
}
} catch (e: Exception) {
println("Error loading page: ${e.message}")
errorMessage = "Error loading page: ${e.message}"
}
// Combine all chapters
bookContent = processedChapters.joinToString("\n")
Timber.d("📚 Final book content processed:")
Timber.d("📚 - Total length: ${bookContent.length}")
Timber.d("📚 - First 100 chars: ${bookContent.take(100)}")
Timber.d("📚 - Last 100 chars: ${bookContent.takeLast(100)}")
// Update chapter list
chapters = book.tableOfContents.tocReferences.map { it.title }
} catch (e: Exception) {
Timber.e(e, "📚 Error loading book")
}
}
LaunchedEffect(bookId) {
scope.launch {
try {
val bookFile = File(context.filesDir, "book_$bookId.epub")
if (!bookFile.exists()) {
errorMessage = "Book file not found"
return@launch
}
val css = """
:root {
--page-width: calc(100vw - ${padding * 2}px);
}
body {
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;
line-height: ${lineHeight}em;
background-color: ${if (isDarkMode) "#1C1B1F" else "#FFFFFF"};
color: ${if (isDarkMode) "#E6E1E5" else "#1C1B1F"};
}
.chapter {
break-inside: avoid;
margin-bottom: 2em;
display: inline-block;
width: 100%;
}
img {
max-width: 100%;
height: auto;
}
p {
margin: 0.5em 0;
text-align: justify;
}
""".trimIndent()
bookFile.inputStream().buffered().use { inputStream ->
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}"
}
BackHandler(enabled = showSettings || showChapterList) {
when {
showSettings -> showSettings = false
showChapterList -> showChapterList = false
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(book?.title ?: "Loading...") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
AnimatedVisibility(
visible = showControls,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
TopAppBar(
title = {
Column {
Text("Chapter ${currentChapter + 1}")
Text(
"Page $currentPage of $totalPages",
style = MaterialTheme.typography.bodyMedium
)
}
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
},
actions = {
IconButton(onClick = { showChapterList = true }) {
Icon(Icons.Default.List, "Chapters")
}
IconButton(onClick = { showSettings = true }) {
Icon(Icons.Default.Settings, "Settings")
}
IconButton(onClick = { isDarkMode = !isDarkMode }) {
Icon(
if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode,
"Toggle theme"
)
}
}
},
actions = {
IconButton(onClick = onOpenSettings) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
)
)
}
}
) { padding ->
Box(
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)
Box(modifier = Modifier.padding(padding)) {
AndroidView(
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
useWideViewPort = true
loadWithOverviewMode = true
defaultFontSize = fontSize.toInt()
Timber.d("📚 WebView settings configured")
}
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")
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
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,
"""
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>$css</style>
</head>
<body>$bookContent</body>
</html>
""".trimIndent(),
"text/html",
"UTF-8",
null
)
webViewRef = this
}
},
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
val screenWidth = this.size.width.toFloat()
when {
offset.x < screenWidth * 0.3f && currentPage > 1 -> {
currentPage--
webViewRef?.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'smooth'})",
null
)
}
offset.x > screenWidth * 0.7f && currentPage < totalPages -> {
currentPage++
webViewRef?.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'smooth'})",
null
)
}
else -> {
showControls = !showControls
}
}
}
}
)
// Settings sheet
if (showSettings) {
ModalBottomSheet(
onDismissRequest = { showSettings = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// WebView for rendering HTML content
AndroidView(
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = false
builtInZoomControls = true
displayZoomControls = false
useWideViewPort = true
loadWithOverviewMode = true
}
webViewClient = WebViewClient()
setBackgroundColor(android.graphics.Color.TRANSPARENT)
}
},
modifier = Modifier.weight(1f),
update = { webView ->
webView.loadDataWithBaseURL(
null,
currentHtml ?: "",
"text/html",
"UTF-8",
Text("Reading Settings", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
Text("Font Size: $fontSize", style = MaterialTheme.typography.bodyMedium)
Slider(
value = fontSize,
onValueChange = {
fontSize = it
// Update WebView font size
webViewRef?.evaluateJavascript(
"document.body.style.fontSize = '${fontSize}px'",
null
)
}
},
valueRange = 12f..24f,
steps = 11
)
// Navigation controls
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = {
if (currentPageIndex > 0) {
currentPageIndex--
loadPage(currentPageIndex)
}
},
enabled = currentPageIndex > 0
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Previous page"
)
}
Text(
text = "Page ${currentPageIndex + 1} of $totalPages",
style = MaterialTheme.typography.bodySmall
// Similar sliders for lineHeight, padding, brightness
// Font family selector
// Color theme selector
}
}
}
// Chapter list
if (showChapterList) {
ModalBottomSheet(
onDismissRequest = { showChapterList = false }
) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(chapters) { chapter ->
ListItem(
headlineContent = { Text(chapter) },
modifier = Modifier.clickable {
// Navigate to chapter
webViewRef?.evaluateJavascript(
"document.getElementById('chapter_$currentChapter').scrollIntoView()",
null
)
showChapterList = false
}
)
IconButton(
onClick = {
if (currentPageIndex < totalPages - 1) {
currentPageIndex++
loadPage(currentPageIndex)
}
},
enabled = currentPageIndex < totalPages - 1
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Next page",
modifier = Modifier.rotate(180f)
)
}
}
}
}
}
// Brightness overlay
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 1f - brightness))
)
}
}
// Save reading progress when leaving
DisposableEffect(Unit) {
onDispose {
scope.launch {
settingsStore.saveLastPosition(bookId, currentPage)
}
}
}
}

View File

@ -2,7 +2,7 @@ package inhale.rip.epook
import androidx.compose.foundation.layout.*
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.runtime.*
import androidx.compose.ui.Modifier
@ -14,15 +14,21 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onNavigateBack: () -> Unit
onNavigateBack: () -> Unit,
settingsStore: SettingsStore
) {
val context = LocalContext.current
val settingsStore = remember { SettingsStore(context) }
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")
val fontSize by settingsStore.fontSize.collectAsState(initial = 16)
val lineHeight by settingsStore.lineHeight.collectAsState(initial = 1.6f)
LaunchedEffect(Unit) {
fontSize = settingsStore.getFontSize()
lineHeight = settingsStore.getLineHeight()
padding = settingsStore.getPadding()
currentFontFamily = settingsStore.getFontFamily()
}
val fontFamilies = listOf("Georgia", "Roboto", "Times New Roman", "Arial", "Verdana")
@ -32,16 +38,16 @@ fun SettingsScreen(
title = { Text("Settings") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
}
) { padding ->
) { contentPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(contentPadding)
.padding(16.dp)
) {
Text(
@ -57,8 +63,9 @@ fun SettingsScreen(
.padding(vertical = 8.dp)
) {
RadioButton(
selected = font == fontFamily,
selected = currentFontFamily == font,
onClick = {
currentFontFamily = font
scope.launch {
settingsStore.updateFontFamily(font)
}
@ -78,10 +85,11 @@ fun SettingsScreen(
style = MaterialTheme.typography.titleMedium
)
Slider(
value = fontSize.toFloat(),
value = fontSize,
onValueChange = {
fontSize = it
scope.launch {
settingsStore.updateFontSize(it.toInt())
settingsStore.updateFontSize(it)
}
},
valueRange = 12f..24f,
@ -97,6 +105,7 @@ fun SettingsScreen(
Slider(
value = lineHeight,
onValueChange = {
lineHeight = it
scope.launch {
settingsStore.updateLineHeight(it)
}
@ -104,6 +113,24 @@ fun SettingsScreen(
valueRange = 1f..2f,
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
)
}
}
}

View File

@ -9,41 +9,29 @@ import kotlinx.coroutines.flow.map
import java.io.File
import inhale.rip.epook.data.AppDatabase
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")
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 bookDao = AppDatabase.getDatabase(context).bookDao()
fun getAllBooks(): Flow<List<Book>> = bookDao.getAllBooks().map { entities ->
entities.mapNotNull { entity ->
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
}
}
entities.map { it.toBook() }
}
suspend fun addBook(book: Book) {
context.dataStore.edit { preferences ->
val currentIds = preferences[bookIdsKey]?.toMutableSet() ?: mutableSetOf()
currentIds.add(book.id)
preferences[bookIdsKey] = currentIds
preferences[stringPreferencesKey("title_${book.id}")] = book.title
}
// Also add to Room database
bookDao.insertBook(book.toEntity())
bookDao.insertBook(BookEntity(
id = book.id,
title = book.title,
coverImagePath = book.coverImageFile?.absolutePath,
filePath = book.filePath,
readingPosition = 0
))
}
suspend fun updateReadingPosition(bookId: String, position: Int) {
@ -55,11 +43,10 @@ class BookStore(private val context: Context) {
}
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")
try {
// Check if book exists in Room database
val bookEntity = bookDao.getBook(bookId)
Log.e(TAG, if (bookEntity != null) {
"Found book in database: ${bookEntity.title} (ID: ${bookEntity.id})"
@ -68,56 +55,25 @@ class BookStore(private val context: Context) {
})
if (bookEntity != null) {
// Try to delete from Room database
try {
bookDao.deleteBook(bookEntity)
Log.e(TAG, "Successfully deleted from database: ${bookEntity.title}")
} catch (e: Exception) {
Log.e(TAG, "Failed to delete from database: ${e.message}", e)
}
}
// File deletion
val bookFile = File(context.filesDir, "book_$bookId.epub")
Log.e(TAG, "Book file path: ${bookFile.absolutePath}")
if (bookFile.exists()) {
Log.e(TAG, "Book file exists, size: ${bookFile.length()} bytes")
val deleted = bookFile.delete()
Log.e(TAG, "Book file deleted: $deleted")
} else {
Log.e(TAG, "Book file does not exist")
}
val coverFile = File(context.filesDir, "cover_$bookId.jpg")
Log.e(TAG, "Cover file path: ${coverFile.absolutePath}")
if (coverFile.exists()) {
Log.e(TAG, "Cover file exists, size: ${coverFile.length()} bytes")
val deleted = coverFile.delete()
Log.e(TAG, "Cover file deleted: $deleted")
} else {
Log.e(TAG, "Cover file does not exist")
}
// DataStore cleanup
Log.e(TAG, "Starting DataStore cleanup")
try {
context.dataStore.edit { preferences ->
val currentIds = preferences[bookIdsKey]?.toMutableSet() ?: mutableSetOf()
Log.e(TAG, "Current IDs in DataStore: $currentIds")
val removed = currentIds.remove(bookId)
Log.e(TAG, "ID removed from DataStore: $removed")
preferences[bookIdsKey] = currentIds
Log.e(TAG, "Updated IDs in DataStore: $currentIds")
Log.e(TAG, "Successfully deleted book from Room database")
preferences.remove(stringPreferencesKey("title_$bookId"))
preferences.remove(stringPreferencesKey("position_$bookId"))
Log.e(TAG, "Removed related preferences for book ID: $bookId")
// 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) {
Log.e(TAG, "Error deleting book files", e)
}
Log.e(TAG, "DataStore cleanup completed successfully")
} catch (e: Exception) {
Log.e(TAG, "Error during DataStore cleanup", e)
}
Log.e(TAG, "🔥 DELETE BOOK COMPLETED SUCCESSFULLY 🔥")
@ -128,4 +84,9 @@ class BookStore(private val context: Context) {
e.printStackTrace()
}
}
suspend fun getBook(bookId: String): EpubBook {
val bookEntity = bookDao.getBook(bookId) ?: throw IllegalArgumentException("Book not found")
return EpubReader().readEpub(FileInputStream(bookEntity.filePath))
}
}

View File

@ -2,45 +2,83 @@ package inhale.rip.epook.data
import android.content.Context
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
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) {
private val fontFamilyKey = stringPreferencesKey("font_family")
private val fontSizeKey = intPreferencesKey("font_size")
private val lineHeightKey = floatPreferencesKey("line_height")
val fontFamily: Flow<String> = context.settingsDataStore.data.map { preferences ->
preferences[fontFamilyKey] ?: "Georgia"
private object PreferencesKeys {
val FONT_SIZE = floatPreferencesKey("font_size")
val LINE_HEIGHT = floatPreferencesKey("line_height")
val PADDING = floatPreferencesKey("padding")
val FONT_FAMILY = stringPreferencesKey("font_family")
fun lastPositionKey(bookId: String) = intPreferencesKey("last_position_$bookId")
}
val fontSize: Flow<Int> = context.settingsDataStore.data.map { preferences ->
preferences[fontSizeKey] ?: 16
suspend fun getFontSize(): Float {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.FONT_SIZE] ?: 16f
}.first()
}
val lineHeight: Flow<Float> = context.settingsDataStore.data.map { preferences ->
preferences[lineHeightKey] ?: 1.6f
suspend fun getLineHeight(): Float {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.LINE_HEIGHT] ?: 1.5f
}.first()
}
suspend fun updateFontFamily(fontFamily: String) {
context.settingsDataStore.edit { preferences ->
preferences[fontFamilyKey] = fontFamily
}
suspend fun getPadding(): Float {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.PADDING] ?: 16f
}.first()
}
suspend fun updateFontSize(size: Int) {
context.settingsDataStore.edit { preferences ->
preferences[fontSizeKey] = size
suspend fun updateFontSize(size: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.FONT_SIZE] = size
}
}
suspend fun updateLineHeight(height: Float) {
context.settingsDataStore.edit { preferences ->
preferences[lineHeightKey] = height
context.dataStore.edit { preferences ->
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
}
}
}

View File

@ -3,40 +3,46 @@ package inhale.rip.epook.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.*
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.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
private val LightColors = lightColorScheme(
primary = Color(0xFF2D5DA1),
onPrimary = Color.White,
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(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
private val DarkColors = darkColorScheme(
primary = Color(0xFFA6C8FF),
onPrimary = Color(0xFF003062),
primaryContainer = Color(0xFF004689),
onPrimaryContainer = Color(0xFFD5E3FF),
secondary = Color(0xFFBEC6DC),
onSecondary = Color(0xFF283041),
secondaryContainer = Color(0xFF3E4759),
background = Color(0xFF1B1B1F),
surface = Color(0xFF1B1B1F),
surfaceVariant = Color(0xFF44474F),
onSurfaceVariant = Color(0xFFC4C6D0)
)
@Composable
fun EpookTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
@ -45,9 +51,17 @@ fun EpookTheme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColors
else -> LightColors
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.surface.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(

View File

@ -6,29 +6,41 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
headlineLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
bodyLarge = TextStyle(
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,
fontSize = 11.sp,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)