Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1122e06437 | |||
| 3295816550 |
@@ -36,7 +36,7 @@ Epook is a sleek, modern EPUB reader built for Android using Jetpack Compose. It
|
|||||||
- Reading progress persistence
|
- Reading progress persistence
|
||||||
- Organized book collection view
|
- Organized book collection view
|
||||||
|
|
||||||
### 🔧 Technical Features
|
### ���� Technical Features
|
||||||
- CSS stylesheet handling
|
- CSS stylesheet handling
|
||||||
- HTML content processing
|
- HTML content processing
|
||||||
- Efficient file management
|
- Efficient file management
|
||||||
@@ -77,3 +77,30 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|||||||
- [epublib](https://github.com/psiegman/epublib) for EPUB processing
|
- [epublib](https://github.com/psiegman/epublib) for EPUB processing
|
||||||
- [Jsoup](https://jsoup.org/) for HTML parsing
|
- [Jsoup](https://jsoup.org/) for HTML parsing
|
||||||
- [Material Design 3](https://m3.material.io/) for design guidelines
|
- [Material Design 3](https://m3.material.io/) for design guidelines
|
||||||
|
|
||||||
|
## 🎨 App Icon Design
|
||||||
|
|
||||||
|
### Primary Design
|
||||||
|
A minimalist, modern book icon featuring:
|
||||||
|
- A stylized open book in Material Design style
|
||||||
|
- Primary color: Deep Purple (#6750A4) with white pages
|
||||||
|
- Subtle gradient background from lighter to darker purple
|
||||||
|
- Rounded corners following Material Design 3 guidelines
|
||||||
|
- Clean, simple lines with minimal detail
|
||||||
|
|
||||||
|
### Specifications
|
||||||
|
- Size: 512x512px (Play Store master icon)
|
||||||
|
- Adaptive Icon Layers:
|
||||||
|
- Foreground: Book icon in white/light purple
|
||||||
|
- Background: Gradient from #6750A4 to #4F378B
|
||||||
|
- Safe zone: 384x384px centered
|
||||||
|
- Corner radius: 100dp (Material 3 spec)
|
||||||
|
|
||||||
|
### Alternative Sizes
|
||||||
|
- 48x48dp (mdpi)
|
||||||
|
- 72x72dp (hdpi)
|
||||||
|
- 96x96dp (xhdpi)
|
||||||
|
- 144x144dp (xxhdpi)
|
||||||
|
- 192x192dp (xxxhdpi)
|
||||||
|
|
||||||
|
### Design Elements
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
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 inhale.rip.epook.data.BookStore
|
import inhale.rip.epook.data.BookStore
|
||||||
import inhale.rip.epook.data.Settings
|
|
||||||
import inhale.rip.epook.data.SettingsStore
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
@@ -50,6 +48,7 @@ import android.webkit.WebResourceRequest
|
|||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebResourceError
|
import android.webkit.WebResourceError
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
// Move the Chapter data class outside the composable
|
// Move the Chapter data class outside the composable
|
||||||
private data class Chapter(
|
private data class Chapter(
|
||||||
@@ -58,6 +57,9 @@ private data class Chapter(
|
|||||||
val resource: Resource
|
val resource: Resource
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// First, add a constant for the swipe area height
|
||||||
|
private const val SWIPE_AREA_HEIGHT = 80 // in dp
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderScreen(
|
fun ReaderScreen(
|
||||||
@@ -66,9 +68,7 @@ fun ReaderScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val bookStore = remember { BookStore(context) }
|
val bookStore = remember { BookStore(context) }
|
||||||
val settingsStore = remember { SettingsStore(context) }
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var currentSettings by remember { mutableStateOf(Settings()) }
|
|
||||||
|
|
||||||
var currentChapterIndex by remember { mutableStateOf(0) }
|
var currentChapterIndex by remember { mutableStateOf(0) }
|
||||||
var chapters by remember { mutableStateOf<List<Chapter>>(emptyList()) }
|
var chapters by remember { mutableStateOf<List<Chapter>>(emptyList()) }
|
||||||
@@ -76,21 +76,12 @@ fun ReaderScreen(
|
|||||||
|
|
||||||
var showControls by remember { mutableStateOf(true) }
|
var showControls by remember { mutableStateOf(true) }
|
||||||
var showChapterList by remember { mutableStateOf(false) }
|
var showChapterList by remember { mutableStateOf(false) }
|
||||||
var showSettings by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var currentX by remember { mutableStateOf(0f) }
|
var currentX by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
var extractedPath by remember { mutableStateOf<String?>(null) }
|
var extractedPath by remember { mutableStateOf<String?>(null) }
|
||||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
try {
|
|
||||||
currentSettings = settingsStore.getSettings()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error loading settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(bookId) {
|
LaunchedEffect(bookId) {
|
||||||
try {
|
try {
|
||||||
currentChapterIndex = bookStore.getReadingPosition(bookId).first()
|
currentChapterIndex = bookStore.getReadingPosition(bookId).first()
|
||||||
@@ -149,11 +140,133 @@ fun ReaderScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(
|
||||||
Box(
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
// WebView without swipe gesture
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
WebView(context).apply {
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError?
|
||||||
|
) {
|
||||||
|
Timber.e("WebView error loading ${request?.url}: ${error?.description}")
|
||||||
|
super.onReceivedError(view, request, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): WebResourceResponse? {
|
||||||
|
val url = request.url.toString()
|
||||||
|
Timber.d("Loading resource: $url")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (url.endsWith(".css") && extractedPath != null) {
|
||||||
|
val possiblePaths = listOf(
|
||||||
|
url.substringAfterLast("/"),
|
||||||
|
"Styles/${url.substringAfterLast("/")}",
|
||||||
|
url.substringAfter("/extracted/")
|
||||||
|
)
|
||||||
|
|
||||||
|
for (cssPath in possiblePaths) {
|
||||||
|
val cssFile = File(extractedPath!!, cssPath)
|
||||||
|
if (cssFile.exists()) {
|
||||||
|
Timber.d("Found CSS file at: ${cssFile.absolutePath}")
|
||||||
|
return WebResourceResponse(
|
||||||
|
"text/css",
|
||||||
|
"UTF-8",
|
||||||
|
cssFile.inputStream()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error intercepting resource request")
|
||||||
|
}
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
builtInZoomControls = true
|
||||||
|
displayZoomControls = false
|
||||||
|
allowFileAccess = true
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
allowFileAccessFromFileURLs = true
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
allowUniversalAccessFromFileURLs = true
|
||||||
|
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||||
|
domStorageEnabled = true
|
||||||
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
|
||||||
|
// Add these specific scrolling optimizations
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
setRenderPriority(WebSettings.RenderPriority.HIGH)
|
||||||
|
|
||||||
|
// Disable features that might cause stuttering
|
||||||
|
loadsImagesAutomatically = true
|
||||||
|
blockNetworkImage = true // Since we're reading locally
|
||||||
|
blockNetworkLoads = true // Since we're reading locally
|
||||||
|
|
||||||
|
// Enable hardware acceleration
|
||||||
|
setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set better scrolling properties
|
||||||
|
overScrollMode = View.OVER_SCROLL_NEVER
|
||||||
|
isVerticalScrollBarEnabled = false // Hide scrollbar for smoother scrolling
|
||||||
|
|
||||||
|
// Set scroll sensitivity
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY)
|
||||||
|
|
||||||
|
// Enable smooth scrolling
|
||||||
|
isScrollContainer = true
|
||||||
|
|
||||||
|
// Set better touch handling
|
||||||
|
isNestedScrollingEnabled = true
|
||||||
|
|
||||||
|
}.also { webView = it }
|
||||||
|
},
|
||||||
|
update = { view ->
|
||||||
|
val chapter = chapters.getOrNull(currentChapterIndex)?.resource
|
||||||
|
chapter?.let { resource ->
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
extractedPath = bookStore.getBook(bookId)?.extractedPath
|
||||||
|
?: throw IllegalStateException("Book not properly extracted")
|
||||||
|
|
||||||
|
val baseUrl = "file://$extractedPath/"
|
||||||
|
val fullUrl = baseUrl + resource.href.replace("../", "")
|
||||||
|
|
||||||
|
Timber.d("Loading chapter from: $fullUrl")
|
||||||
|
Timber.d("Base URL: $baseUrl")
|
||||||
|
|
||||||
|
view.loadUrl(fullUrl)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error loading chapter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(bottom = if (showControls) 80.dp else 0.dp)
|
.padding(bottom = if (showControls) SWIPE_AREA_HEIGHT.dp else 0.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add swipe area at the bottom with tap gesture
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(SWIPE_AREA_HEIGHT.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = { showControls = !showControls }
|
onTap = { showControls = !showControls }
|
||||||
@@ -166,7 +279,6 @@ fun ReaderScreen(
|
|||||||
initialX = offset.x
|
initialX = offset.x
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
// Implement drag threshold for swipe
|
|
||||||
val dragThreshold = size.width * 0.2f // 20% of screen width
|
val dragThreshold = size.width * 0.2f // 20% of screen width
|
||||||
val dragDistance = initialX - currentX
|
val dragDistance = initialX - currentX
|
||||||
|
|
||||||
@@ -180,99 +292,14 @@ fun ReaderScreen(
|
|||||||
currentChapterIndex--
|
currentChapterIndex--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentX = 0f
|
||||||
}
|
}
|
||||||
) { change, _ ->
|
) { change, dragAmount ->
|
||||||
currentX = change.position.x
|
currentX = change.position.x
|
||||||
change.consume()
|
change.consume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
)
|
||||||
AndroidView(
|
|
||||||
factory = { context ->
|
|
||||||
WebView(context).apply {
|
|
||||||
webViewClient = object : WebViewClient() {
|
|
||||||
override fun onReceivedError(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest?,
|
|
||||||
error: WebResourceError?
|
|
||||||
) {
|
|
||||||
Timber.e("WebView error loading ${request?.url}: ${error?.description}")
|
|
||||||
super.onReceivedError(view, request, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldInterceptRequest(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest
|
|
||||||
): WebResourceResponse? {
|
|
||||||
val url = request.url.toString()
|
|
||||||
Timber.d("Loading resource: $url")
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (url.endsWith(".css") && extractedPath != null) {
|
|
||||||
val possiblePaths = listOf(
|
|
||||||
url.substringAfterLast("/"),
|
|
||||||
"Styles/${url.substringAfterLast("/")}",
|
|
||||||
url.substringAfter("/extracted/")
|
|
||||||
)
|
|
||||||
|
|
||||||
for (cssPath in possiblePaths) {
|
|
||||||
val cssFile = File(extractedPath!!, cssPath)
|
|
||||||
if (cssFile.exists()) {
|
|
||||||
Timber.d("Found CSS file at: ${cssFile.absolutePath}")
|
|
||||||
return WebResourceResponse(
|
|
||||||
"text/css",
|
|
||||||
"UTF-8",
|
|
||||||
cssFile.inputStream()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error intercepting resource request")
|
|
||||||
}
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.apply {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
builtInZoomControls = true
|
|
||||||
displayZoomControls = false
|
|
||||||
allowFileAccess = true
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
allowFileAccessFromFileURLs = true
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
allowUniversalAccessFromFileURLs = true
|
|
||||||
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
|
||||||
domStorageEnabled = true
|
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
}
|
|
||||||
}.also { webView = it }
|
|
||||||
},
|
|
||||||
update = { view ->
|
|
||||||
val chapter = chapters.getOrNull(currentChapterIndex)?.resource
|
|
||||||
chapter?.let { resource ->
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
extractedPath = bookStore.getBook(bookId)?.extractedPath
|
|
||||||
?: throw IllegalStateException("Book not properly extracted")
|
|
||||||
|
|
||||||
val baseUrl = "file://$extractedPath/"
|
|
||||||
val fullUrl = baseUrl + resource.href.replace("../", "")
|
|
||||||
|
|
||||||
Timber.d("Loading chapter from: $fullUrl")
|
|
||||||
Timber.d("Base URL: $baseUrl")
|
|
||||||
|
|
||||||
view.loadUrl(fullUrl)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error loading chapter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controls overlay
|
// Controls overlay
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
@@ -340,30 +367,6 @@ fun ReaderScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapter list dialog
|
// Chapter list dialog
|
||||||
@@ -414,164 +417,4 @@ fun ReaderScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Settings Dialog
|
|
||||||
if (showSettings) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showSettings = false },
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
"Reader Settings",
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
// Font Size Slider
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Font Size",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"${currentSettings.fontSize.toInt()}sp",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Slider(
|
|
||||||
value = currentSettings.fontSize,
|
|
||||||
onValueChange = { newSize ->
|
|
||||||
scope.launch {
|
|
||||||
val newSettings = currentSettings.copy(fontSize = newSize)
|
|
||||||
settingsStore.saveSettings(newSettings)
|
|
||||||
currentSettings = newSettings
|
|
||||||
}
|
|
||||||
},
|
|
||||||
valueRange = 12f..24f,
|
|
||||||
steps = 11
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Line Height Slider
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Line Height",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"%.1fx".format(currentSettings.lineHeight),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Slider(
|
|
||||||
value = currentSettings.lineHeight,
|
|
||||||
onValueChange = { newHeight ->
|
|
||||||
scope.launch {
|
|
||||||
val newSettings = currentSettings.copy(lineHeight = newHeight)
|
|
||||||
settingsStore.saveSettings(newSettings)
|
|
||||||
currentSettings = newSettings
|
|
||||||
}
|
|
||||||
},
|
|
||||||
valueRange = 1f..2f,
|
|
||||||
steps = 9
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Padding Slider
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Margin",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"${currentSettings.padding.toInt()}dp",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Slider(
|
|
||||||
value = currentSettings.padding,
|
|
||||||
onValueChange = { newPadding ->
|
|
||||||
scope.launch {
|
|
||||||
val newSettings = currentSettings.copy(padding = newPadding)
|
|
||||||
settingsStore.saveSettings(newSettings)
|
|
||||||
currentSettings = newSettings
|
|
||||||
}
|
|
||||||
},
|
|
||||||
valueRange = 8f..32f,
|
|
||||||
steps = 11
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font Family Dropdown
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
"Font Family",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
val fontFamilies = listOf("Georgia", "Roboto", "Times New Roman", "Arial", "Verdana")
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = !expanded }
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = currentSettings.fontFamily,
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
|
||||||
modifier = Modifier
|
|
||||||
.menuAnchor()
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }
|
|
||||||
) {
|
|
||||||
fontFamilies.forEach { font ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(font) },
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
val newSettings = currentSettings.copy(fontFamily = font)
|
|
||||||
settingsStore.saveSettings(newSettings)
|
|
||||||
currentSettings = newSettings
|
|
||||||
}
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { showSettings = false }) {
|
|
||||||
Text("Close")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user