better UI, +windows1251 encoding support
This commit is contained in:
1 parent
ddf68c1e6a
commit
da88a6e221
15 files changed
+407
-14
No files matched your search
@@ -268,6 +268,37 @@ actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingS
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val shareFile = openLibraryDatabase().useLibrary { db ->
|
||||
val file = db.files.getLibraryFile(fileId) ?: return@useLibrary null
|
||||
val bytes = readStorageUriBytes(file.storageUri ?: return@useLibrary null) ?: return@useLibrary null
|
||||
val shareDir = File(appContext.cacheDir, "shared-books").also { it.mkdirs() }
|
||||
File(shareDir, file.shareFileName()).also { it.writeBytes(bytes) }
|
||||
} ?: return@withContext false
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
appContext,
|
||||
"${appContext.packageName}.imageviewer.fileprovider",
|
||||
shareFile,
|
||||
)
|
||||
val mimeType = shareFile.bookMimeType()
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = mimeType
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooser = Intent.createChooser(intent, "Share book").apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
appContext.startActivity(chooser)
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
|
||||
openLibraryDatabase().useLibrary { db ->
|
||||
val file = db.files.get(fileId) ?: return@useLibrary BookInfoExtras()
|
||||
@@ -528,6 +559,26 @@ private fun String.requiresExternalFileAccess(): Boolean {
|
||||
private fun String.isSupportedBookFile(): Boolean =
|
||||
endsWith(".fb2", ignoreCase = true) || endsWith(".fb2.zip", ignoreCase = true)
|
||||
|
||||
private fun LibraryFileRecord.shareFileName(): String {
|
||||
val raw = originalFilename?.takeIf { it.isNotBlank() }
|
||||
?: title?.takeIf { it.isNotBlank() }?.let { title ->
|
||||
val extension = when {
|
||||
format.equals("fb2.zip", ignoreCase = true) || storageUri?.endsWith(".fb2.zip", ignoreCase = true) == true -> ".fb2.zip"
|
||||
else -> ".fb2"
|
||||
}
|
||||
"$title$extension"
|
||||
}
|
||||
?: "book.fb2"
|
||||
return raw.replace(Regex("""[\\/:*?"<>|]+"""), "_")
|
||||
}
|
||||
|
||||
private fun File.bookMimeType(): String =
|
||||
if (name.endsWith(".zip", ignoreCase = true)) {
|
||||
"application/zip"
|
||||
} else {
|
||||
"application/x-fictionbook+xml"
|
||||
}
|
||||
|
||||
private fun displayNameFor(uri: Uri): String =
|
||||
appContext.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) cursor.getString(0) else null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="clipboard_images" path="clipboard-images/"/>
|
||||
<cache-path name="shared_books" path="shared-books/"/>
|
||||
</paths>
|
||||
@@ -166,6 +166,9 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
|
||||
message = current.message,
|
||||
)
|
||||
},
|
||||
onDeleted = { message ->
|
||||
state = AppState.Library(emptyList(), current.scanPath, message)
|
||||
},
|
||||
onBack = {
|
||||
state = AppState.Library(emptyList(), current.scanPath, current.message)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.sergeych.toread
|
||||
|
||||
internal data class LibraryDeleteResult(
|
||||
val deleted: Boolean,
|
||||
val message: String,
|
||||
)
|
||||
|
||||
internal suspend fun deleteLibraryBook(fileId: String, title: String): LibraryDeleteResult {
|
||||
val deleted = runCatching { deleteLibraryItem(fileId) }.getOrDefault(false)
|
||||
return LibraryDeleteResult(
|
||||
deleted = deleted,
|
||||
message = if (deleted) "Removed $title." else "Could not remove $title.",
|
||||
)
|
||||
}
|
||||
@@ -108,6 +108,10 @@ expect suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingP
|
||||
|
||||
expect suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean
|
||||
|
||||
expect suspend fun shareLibraryBookFile(fileId: String): Boolean
|
||||
|
||||
expect suspend fun viewLibraryBookFile(fileId: String): Boolean
|
||||
|
||||
expect suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras
|
||||
|
||||
expect suspend fun loadActiveReadingFileId(): String?
|
||||
|
||||
@@ -364,9 +364,9 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val deleted = runCatching { deleteLibraryItem(item.fileId) }.getOrDefault(false)
|
||||
message = if (deleted) "Removed ${item.title}." else "Could not remove ${item.title}."
|
||||
if (deleted) {
|
||||
val result = deleteLibraryBook(item.fileId, item.title)
|
||||
message = result.message
|
||||
if (result.deleted) {
|
||||
items = items.filterNot { it.fileId == item.fileId }
|
||||
searchResults = searchResults.filterNot { it.fileId == item.fileId }
|
||||
coverCache.remove(item.fileId)
|
||||
|
||||
@@ -90,9 +90,9 @@ internal fun ContinuousBookReader(
|
||||
val hyphenation = remember { HyphenationRegistry() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val contentPadding = if (isAndroidPlatform()) {
|
||||
PaddingValues(start = 6.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
|
||||
PaddingValues(start = 0.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
|
||||
} else {
|
||||
PaddingValues(horizontal = 8.dp, vertical = 6.dp)
|
||||
PaddingValues(horizontal = 4.dp, vertical = 6.dp)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
@@ -461,8 +461,8 @@ private fun ReaderText(
|
||||
@Composable
|
||||
private fun readerParagraphTextStyle(language: String?): TextStyle =
|
||||
MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight(350),
|
||||
fontSize = 21.sp,
|
||||
fontWeight = if( isAndroidPlatform()) FontWeight(350) else FontWeight.Normal,
|
||||
fontSize = if( isAndroidPlatform()) 21.sp else 18.sp,
|
||||
lineHeight = 28.sp,
|
||||
hyphens = if (isAndroidPlatform()) Hyphens.Auto else Hyphens.Unspecified,
|
||||
lineBreak = if (isAndroidPlatform()) LineBreak.Paragraph else LineBreak.Unspecified,
|
||||
|
||||
@@ -12,13 +12,19 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -47,13 +53,36 @@ internal fun BookView(
|
||||
onImageOpen: (ViewedBookImage) -> Unit,
|
||||
onThemeToggle: () -> Unit,
|
||||
onBookInfo: () -> Unit,
|
||||
onDeleted: (String) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val stats = remember(book) { BookStats.from(book) }
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var restored by remember(fileId) { mutableStateOf(false) }
|
||||
var markedRead by remember(fileId) { mutableStateOf(false) }
|
||||
val platformName = getPlatform().name
|
||||
val showShareAction = platformName.startsWith("Android")
|
||||
val showViewFileAction = platformName.startsWith("Java")
|
||||
|
||||
fun showMessage(message: String) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(message, duration = SnackbarDuration.Short)
|
||||
}
|
||||
}
|
||||
|
||||
fun setReadingStatus(status: BookReadingStatus, successMessage: String) {
|
||||
scope.launch {
|
||||
if (markLibraryReadingStatus(fileId, status)) {
|
||||
if (status == BookReadingStatus.READ) markedRead = true
|
||||
if (status == BookReadingStatus.NEW) markedRead = false
|
||||
showMessage(successMessage)
|
||||
} else {
|
||||
showMessage("Could not update book.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(fileId) {
|
||||
markLibraryReadingStatus(fileId, BookReadingStatus.READING)
|
||||
@@ -91,6 +120,7 @@ internal fun BookView(
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
CompactReaderTopBar(
|
||||
title = book.title,
|
||||
@@ -104,6 +134,40 @@ internal fun BookView(
|
||||
onBookInfo()
|
||||
}
|
||||
},
|
||||
onMarkAsRead = {
|
||||
setReadingStatus(BookReadingStatus.READ, "Marked as read.")
|
||||
},
|
||||
onNotInterested = {
|
||||
setReadingStatus(BookReadingStatus.NOT_INTERESTED, "Marked as not interested.")
|
||||
},
|
||||
onClearMarks = {
|
||||
setReadingStatus(BookReadingStatus.NEW, "Cleared marks.")
|
||||
},
|
||||
showShareAction = showShareAction,
|
||||
onShare = {
|
||||
scope.launch {
|
||||
val shared = shareLibraryBookFile(fileId)
|
||||
showMessage(if (shared) "Share opened." else "Could not share book.")
|
||||
}
|
||||
},
|
||||
showViewFileAction = showViewFileAction,
|
||||
onViewFile = {
|
||||
scope.launch {
|
||||
val opened = viewLibraryBookFile(fileId)
|
||||
showMessage(if (opened) "Opened file location." else "Could not open file location.")
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
scope.launch {
|
||||
val result = deleteLibraryBook(fileId, book.title)
|
||||
if (result.deleted) {
|
||||
saveActiveReadingFileId(null)
|
||||
onDeleted(result.message)
|
||||
} else {
|
||||
showMessage(result.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
onBack = {
|
||||
scope.launch {
|
||||
saveLibraryReadingPosition(
|
||||
@@ -139,8 +203,18 @@ private fun CompactReaderTopBar(
|
||||
title: String,
|
||||
onThemeToggle: () -> Unit,
|
||||
onBookInfo: () -> Unit,
|
||||
onMarkAsRead: () -> Unit,
|
||||
onNotInterested: () -> Unit,
|
||||
onClearMarks: () -> Unit,
|
||||
showShareAction: Boolean,
|
||||
onShare: () -> Unit,
|
||||
showViewFileAction: Boolean,
|
||||
onViewFile: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
|
||||
ThemedTopBarSurface {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||
@@ -158,12 +232,74 @@ private fun CompactReaderTopBar(
|
||||
IconButton(onClick = onThemeToggle) {
|
||||
Icon(Icons.Filled.Palette, contentDescription = "Theme")
|
||||
}
|
||||
IconButton(onClick = onBookInfo) {
|
||||
Icon(Icons.Filled.Info, contentDescription = "Properties")
|
||||
}
|
||||
IconButton(onClick = { }) {
|
||||
Icon(Icons.AutoMirrored.Filled.VolumeUp, contentDescription = "Read aloud")
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { menuOpen = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = "Book reader menu")
|
||||
}
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Info...") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onBookInfo()
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Mark as read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onMarkAsRead()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Not interested") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onNotInterested()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear marks") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onClearMarks()
|
||||
},
|
||||
)
|
||||
if (showShareAction || showViewFileAction) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (showShareAction) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Share") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onShare()
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showViewFileAction) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("View file") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onViewFile()
|
||||
},
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Delete") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onDelete()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import net.sergeych.toread.storage.ReadingStateRecord
|
||||
import net.sergeych.toread.storage.jdbc.H2LibraryDatabase
|
||||
import net.sergeych.toread.storage.jdbc.LibraryScanner
|
||||
import org.jetbrains.skia.Image
|
||||
import java.awt.Desktop
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
@@ -218,6 +219,24 @@ actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingS
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
if (!Desktop.isDesktopSupported()) return@withContext false
|
||||
val file = openLibraryDatabase().useLibrary { db ->
|
||||
db.files.get(fileId)?.storageUri?.let(::File)?.takeIf { it.isFile }
|
||||
} ?: return@withContext false
|
||||
val desktop = Desktop.getDesktop()
|
||||
if (desktop.isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
|
||||
desktop.browseFileDirectory(file)
|
||||
} else {
|
||||
desktop.open(file.parentFile ?: file)
|
||||
}
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
|
||||
openLibraryDatabase().useLibrary { db ->
|
||||
val file = db.files.get(fileId) ?: return@useLibrary BookInfoExtras()
|
||||
|
||||
@@ -46,6 +46,10 @@ actual suspend fun saveLibraryReadingPosition(fileId: String, position: ReadingP
|
||||
|
||||
actual suspend fun markLibraryReadingStatus(fileId: String, status: BookReadingStatus): Boolean = false
|
||||
|
||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = false
|
||||
|
||||
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = BookInfoExtras()
|
||||
|
||||
actual suspend fun loadActiveReadingFileId(): String? = null
|
||||
|
||||
Reference in new issue
Block a user