Linux support (#381)

* Add desktop release CI and support for Arch Linux packaging

* Make Gradle wrapper executable in desktop-release workflow

* Make Gradle wrapper executable in desktop-release workflow

* Make Gradle wrapper executable in desktop-release workflow

* Configure Gradle and update Java environment in desktop-release workflow

* Update Java setup and AUR packaging in desktop release workflow

* Update Java setup and AUR packaging in desktop release workflow

* Add MSIX packaging support for Windows desktop distribution

* Update AUR packaging metadata and validation

* Use spine toc attribute for NCX resolution

* crash fixes

* Implement automatic discovery and injection of EPUB font face siblings

* Enhance custom font support with family grouping and variable font handling

* Optimize metadata loading and improve TTS highlighting

* Add keyboard navigation support for EPUB reader

* Refine PDF spread page sizing to respect aspect ratios

* Implement responsive maximum height for reader popups and sheets

* Handle TTS generation failures by skipping problematic chunks

* Refactor PDF tile rendering logic and zoom indicator behavior

* Prefer block and offset locators over page index in native vertical flow

* Implement save and share actions for original book files

* Add Estonian language support

* Implement temporary viewing mode for external files

* Implement direct opening for temporary external files without library persistence

* fix failing tests

* Import SharedFileCapabilities in DesktopLibraryUi

* Improve native vertical reader progress, persistence, and image support

* Center target in viewport for native vertical reader and support animated scrolling
This commit is contained in:
Aryan 2026-06-14 13:43:49 +05:30 committed by GitHub
parent a13d6599d1
commit 625a4d5d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 6012 additions and 687 deletions

View file

@ -133,6 +133,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.util.UnstableApi
@ -271,6 +272,36 @@ fun LibraryScreen(
var showDeleteShelvesDialog by remember { mutableStateOf(false) }
var showInfoDialog by remember { mutableStateOf(false) }
var itemForInfoDialog by remember { mutableStateOf<RecentFileItem?>(null) }
var pendingSaveOriginalItem by remember { mutableStateOf<RecentFileItem?>(null) }
val saveOriginalLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/octet-stream")
) { uri ->
val item = pendingSaveOriginalItem
pendingSaveOriginalItem = null
if (uri != null && item?.uriString != null) {
viewModel.saveOriginalFile(item.uriString.toUri(), uri)
}
}
fun saveOriginalItem(item: RecentFileItem) {
if (!item.canExportOriginalFile()) return
pendingSaveOriginalItem = item
saveOriginalLauncher.launch(item.suggestedOriginalFileName())
}
fun shareOriginalItem(item: RecentFileItem) {
val uriString = item.uriString ?: return
if (!item.canExportOriginalFile()) return
scope.launch {
viewModel.shareOriginalFile(
activityContext = context,
sourceUri = uriString.toUri(),
fileType = item.type,
filename = item.suggestedOriginalFileName()
)
}
}
BackHandler(enabled = isContextualModeActive) {
viewModel.clearContextualAction()
@ -317,6 +348,12 @@ fun LibraryScreen(
showInfoDialog = true
}
},
onSaveClick = selectedItems.singleOrNull()
?.takeIf { it.canExportOriginalFile() }
?.let { item -> { saveOriginalItem(item) } },
onShareClick = selectedItems.singleOrNull()
?.takeIf { it.canExportOriginalFile() }
?.let { item -> { shareOriginalItem(item) } },
onDeleteClick = { showDeleteConfirmDialog = true },
onSelectAllClick = { viewModel.selectAllLibraryFiles() },
onShelfClick = viewModel::onShelfClick,
@ -397,28 +434,17 @@ fun LibraryScreen(
)
}
itemForInfoDialog?.let { item ->
if (showInfoDialog) {
FileInfoDialog(
item = item,
usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
onDismiss = {
showInfoDialog = false
itemForInfoDialog = null
},
onSaveMetadata = { metadata ->
viewModel.updateBookMetadata(item.bookId, metadata)
},
onSaveDisplayName = { name ->
viewModel.updateCustomName(item.bookId, name)
},
onRestoreMetadata = {
viewModel.restoreOriginalBookMetadata(item.bookId)
},
onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
)
}
}
HydratedFileInfoDialog(
item = itemForInfoDialog,
isVisible = showInfoDialog,
uiState = uiState,
viewModel = viewModel,
onDismiss = {
showInfoDialog = false
itemForInfoDialog = null
},
onOpenTags = { bookId -> viewModel.openTagSelection(setOf(bookId)) }
)
CustomTopBanner(bannerMessage = uiState.bannerMessage)
}
}
@ -436,10 +462,42 @@ fun ShelfScreen(
val sortOrder = uiState.sortOrder
val showRenameDialogFor = uiState.showRenameShelfDialogFor
val showDeleteDialogFor = uiState.showDeleteShelfDialogFor
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showRemoveFromShelfDialog by remember { mutableStateOf(false) }
var showInfoDialog by remember { mutableStateOf(false) }
var itemForInfoDialog by remember { mutableStateOf<RecentFileItem?>(null) }
var pendingSaveOriginalItem by remember { mutableStateOf<RecentFileItem?>(null) }
val saveOriginalLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/octet-stream")
) { uri ->
val item = pendingSaveOriginalItem
pendingSaveOriginalItem = null
if (uri != null && item?.uriString != null) {
viewModel.saveOriginalFile(item.uriString.toUri(), uri)
}
}
fun saveOriginalItem(item: RecentFileItem) {
if (!item.canExportOriginalFile()) return
pendingSaveOriginalItem = item
saveOriginalLauncher.launch(item.suggestedOriginalFileName())
}
fun shareOriginalItem(item: RecentFileItem) {
val uriString = item.uriString ?: return
if (!item.canExportOriginalFile()) return
scope.launch {
viewModel.shareOriginalFile(
activityContext = context,
sourceUri = uriString.toUri(),
fileType = item.type,
filename = item.suggestedOriginalFileName()
)
}
}
BackHandler(enabled = true) {
when {
@ -491,6 +549,12 @@ fun ShelfScreen(
showInfoDialog = true
}
},
onSaveClick = selectedItems.singleOrNull()
?.takeIf { it.canExportOriginalFile() }
?.let { item -> { saveOriginalItem(item) } },
onShareClick = selectedItems.singleOrNull()
?.takeIf { it.canExportOriginalFile() }
?.let { item -> { shareOriginalItem(item) } },
onDeleteClick = { showRemoveFromShelfDialog = true },
onRenameShelf = { viewModel.showRenameShelfDialog(currentShelf.id) },
onDeleteShelf = { viewModel.showDeleteShelfDialog(currentShelf.id) },
@ -531,19 +595,14 @@ fun ShelfScreen(
)
}
itemForInfoDialog?.let { item ->
if (showInfoDialog) {
FileInfoDialog(
item = item,
usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
onDismiss = { showInfoDialog = false; itemForInfoDialog = null },
onSaveMetadata = { metadata -> viewModel.updateBookMetadata(item.bookId, metadata) },
onSaveDisplayName = { name -> viewModel.updateCustomName(item.bookId, name) },
onRestoreMetadata = { viewModel.restoreOriginalBookMetadata(item.bookId) },
onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
)
}
}
HydratedFileInfoDialog(
item = itemForInfoDialog,
isVisible = showInfoDialog,
uiState = uiState,
viewModel = viewModel,
onDismiss = { showInfoDialog = false; itemForInfoDialog = null },
onOpenTags = { bookId -> viewModel.openTagSelection(setOf(bookId)) }
)
CustomTopBanner(bannerMessage = uiState.bannerMessage)
}
}
@ -578,6 +637,8 @@ fun LibraryScreenContent(
onItemClick: (RecentFileItem) -> Unit,
onItemLongClick: (RecentFileItem) -> Unit,
onInfoClick: () -> Unit,
onSaveClick: (() -> Unit)?,
onShareClick: (() -> Unit)?,
onDeleteClick: () -> Unit,
onSelectAllClick: () -> Unit,
onShelfClick: (Shelf) -> Unit,
@ -640,6 +701,8 @@ fun LibraryScreenContent(
onTagClick = onTagClick,
onPinClick = onPinClick,
onInfoClick = onInfoClick,
onSaveClick = onSaveClick,
onShareClick = onShareClick,
onDeleteClick = onDeleteClick,
onSelectAllClick = onSelectAllClick
)
@ -1046,6 +1109,8 @@ private fun ShelfDetailScreen(
onClearSelection: () -> Unit,
onTagClick: () -> Unit,
onInfoClick: () -> Unit,
onSaveClick: (() -> Unit)?,
onShareClick: (() -> Unit)?,
onDeleteClick: () -> Unit,
onRenameShelf: () -> Unit,
onDeleteShelf: () -> Unit,
@ -1128,6 +1193,8 @@ private fun ShelfDetailScreen(
onNavIconClick = onClearSelection,
onTagClick = onTagClick,
onInfoClick = onInfoClick,
onSaveClick = onSaveClick,
onShareClick = onShareClick,
onDeleteClick = onDeleteClick
)
} else if (isSearchActive) {