Skip to content

Commit

Permalink
Added ability to select particular PDF pages for export by text, or r…
Browse files Browse the repository at this point in the history
…anges by #1581
  • Loading branch information
T8RIN committed Jan 15, 2025
1 parent cbe741c commit 1a750d8
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 30 deletions.
2 changes: 2 additions & 0 deletions core/resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1556,4 +1556,6 @@
<string name="error_while_saving">Error while saving attempt, try to change output folder</string>
<string name="filename_is_not_set">Filename is not set</string>
<string name="none">None</string>
<string name="custom_pages">Custom Pages</string>
<string name="pages_selection">Pages Selection</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AddPhotoAlternate
import androidx.compose.material.icons.rounded.FileOpen
import androidx.compose.material.icons.rounded.Pages
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
Expand All @@ -64,12 +65,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.resources.icons.MiniEdit
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberFilePicker
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
Expand All @@ -84,13 +87,17 @@ import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.QualitySel
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.ExitWithoutSavingDialog
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.LoadingDialog
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedAlertDialog
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedButton
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedFloatingActionButton
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedModalBottomSheet
import ru.tech.imageresizershrinker.core.ui.widget.preferences.PreferenceItem
import ru.tech.imageresizershrinker.core.ui.widget.text.TitleItem
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.components.PageInputField
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.components.PdfToImagesPreference
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.components.PdfToolsContentImpl
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.components.PreviewPdfPreference
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.components.formatPageOutput
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.screenLogic.PdfToolsComponent

@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -282,9 +289,9 @@ fun PdfToolsContent(
)
}
if (pdfType !is Screen.PdfTools.Type.Preview) {
val visible by remember(component.pdfToImageState?.pages, pdfType) {
val visible by remember(component.pdfToImageState?.selectedPages, pdfType) {
derivedStateOf {
(component.pdfToImageState?.pages?.size != 0 && pdfType is Screen.PdfTools.Type.PdfToImages) || pdfType !is Screen.PdfTools.Type.PdfToImages
(component.pdfToImageState?.selectedPages?.size != 0 && pdfType is Screen.PdfTools.Type.PdfToImages) || pdfType !is Screen.PdfTools.Type.PdfToImages
}
}
if (visible) {
Expand Down Expand Up @@ -381,6 +388,74 @@ fun PdfToolsContent(
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val context = LocalContext.current
var showSelector by rememberSaveable {
mutableStateOf(false)
}
PreferenceItem(
title = stringResource(R.string.pages_selection),
subtitle = remember(component.pdfToImageState) {
derivedStateOf {
component.pdfToImageState?.takeIf { it.selectedPages.isNotEmpty() }
?.let {
if (it.selectedPages.size == it.pagesCount) {
context.getString(R.string.all)
} else {
formatPageOutput(it.selectedPages)
}
} ?: context.getString(R.string.none)
}
}.value,
onClick = {
showSelector = true
},
modifier = Modifier.fillMaxWidth(),
startIcon = Icons.Rounded.Pages,
endIcon = Icons.Rounded.MiniEdit
)
var pages by rememberSaveable(showSelector) {
mutableStateOf(component.pdfToImageState?.selectedPages ?: emptyList())
}
EnhancedAlertDialog(
visible = showSelector,
onDismissRequest = { showSelector = false },
title = {
Text(stringResource(R.string.pages_selection))
},
icon = {
Icon(
imageVector = Icons.Rounded.Pages,
contentDescription = null
)
},
text = {
PageInputField(
selectedPages = pages,
onPagesChanged = { pages = it }
)
},
dismissButton = {
EnhancedButton(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
onClick = {
showSelector = false
}
) {
Text(stringResource(R.string.close))
}
},
confirmButton = {
EnhancedButton(
onClick = {
component.updatePdfToImageSelection(pages)
showSelector = false
}
) {
Text(stringResource(R.string.apply))
}
}
)
Spacer(Modifier.height(8.dp))
PresetSelector(
value = component.presetSelected,
includeTelegramOption = false,
Expand All @@ -392,19 +467,15 @@ fun PdfToolsContent(
showWarning = component.showOOMWarning
)
if (component.imageInfo.imageFormat.canChangeCompressionValue) {
Spacer(
Modifier.height(8.dp)
)
Spacer(Modifier.height(8.dp))
}
QualitySelector(
imageFormat = component.imageInfo.imageFormat,
enabled = true,
quality = component.imageInfo.quality,
onQualityChange = component::setQuality
)
Spacer(
Modifier.height(8.dp)
)
Spacer(Modifier.height(8.dp))
ImageFormatSelector(
value = component.imageInfo.imageFormat,
onValueChange = component::updateImageFormat
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/

package ru.tech.imageresizershrinker.feature.pdf_tools.presentation.components

import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
import ru.tech.imageresizershrinker.core.ui.widget.text.RoundedTextField

@Composable
internal fun PageInputField(
selectedPages: List<Int>,
onPagesChanged: (List<Int>) -> Unit
) {
var text by remember {
mutableStateOf(formatPageOutput(selectedPages))
}

RoundedTextField(
value = text,
onValueChange = {
text = it
val parsedPages = parsePageInput(it)
onPagesChanged(parsedPages)
},
textStyle = LocalTextStyle.current.copy(
textAlign = TextAlign.Start
),
label = stringResource(R.string.custom_pages),
modifier = Modifier
.container(
resultPadding = 0.dp,
shape = RoundedCornerShape(20.dp)
)
.padding(8.dp),
singleLine = false
)
}

internal fun parsePageInput(input: String): List<Int> {
val pages = mutableSetOf<Int>()
val regex = "\\d+(-\\d+)?".toRegex()
regex.findAll(input).forEach { match ->
val rangeParts = match.value.split("-").mapNotNull { it.toIntOrNull() }
when (rangeParts.size) {
1 -> pages.add(rangeParts[0] - 1)
2 -> if (rangeParts[0] <= rangeParts[1]) {
pages.addAll((rangeParts[0] - 1)..(rangeParts[1] - 1))
}
}
}
return pages.sorted()
}

internal fun formatPageOutput(pages: List<Int>): String {
if (pages.isEmpty()) return ""
val result = mutableListOf<String>()
var start = pages[0]
var prev = pages[0]
for (i in 1 until pages.size) {
if (pages[i] != prev + 1) {
result.add(if (start == prev) "${start + 1}" else "${start + 1}-${prev + 1}")
start = pages[i]
}
prev = pages[i]
}
result.add(if (start == prev) "${start + 1}" else "${start + 1}-${prev + 1}")
return result.joinToString(", ")
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ import android.net.Uri

data class PdfToImageState(
val uri: Uri,
val pages: List<Int>
val pagesCount: Int,
val selectedPages: List<Int>
)
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,17 @@ internal fun PdfToolsContentImpl(
if (component.pdfType == null) {
TopAppBarEmoji()
} else {
val pagesSize = component.pdfToImageState?.pages?.size
val selectedPagesSize = component.pdfToImageState?.selectedPages?.size
val visible by remember(
component.pdfToImageState?.pages,
component.pdfToImageState?.selectedPages,
component.pdfType
) {
derivedStateOf {
(pagesSize != 0 && component.pdfType is Screen.PdfTools.Type.PdfToImages)
(selectedPagesSize != 0 && component.pdfType is Screen.PdfTools.Type.PdfToImages)
}
}
AnimatedVisibility(
visible = component.pdfType is Screen.PdfTools.Type.PdfToImages,
visible = component.pdfType is Screen.PdfTools.Type.PdfToImages && selectedPagesSize != component.pdfToImageState?.pagesCount,
enter = fadeIn() + scaleIn() + expandHorizontally(),
exit = fadeOut() + scaleOut() + shrinkHorizontally()
) {
Expand Down Expand Up @@ -189,10 +189,10 @@ internal fun PdfToolsContentImpl(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
pagesSize?.takeIf { it != 0 }?.let {
selectedPagesSize?.takeIf { it != 0 }?.let {
Spacer(Modifier.width(8.dp))
Text(
text = pagesSize.toString(),
text = selectedPagesSize.toString(),
fontSize = 20.sp,
fontWeight = FontWeight.Medium
)
Expand Down Expand Up @@ -361,7 +361,7 @@ internal fun PdfToolsContentImpl(
enableSelection = true,
selectAllToggle = selectAllToggle,
deselectAllToggle = deselectAllToggle,
selectedPages = component.pdfToImageState?.pages
selectedPages = component.pdfToImageState?.selectedPages
?: emptyList(),
updateSelectedPages = component::updatePdfToImageSelection,
spacing = 4.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,12 @@ fun PdfViewer(
}
val pageCount by remember(renderer) { derivedStateOf { renderer?.pageCount ?: 0 } }

val selectedItems = remember(uri) {
val key by remember(uri, selectedPages) {
derivedStateOf {
uri to selectedPages
}
}
val selectedItems = remember(key) {
mutableStateOf(selectedPages.toSet())
}
LaunchedEffect(selectedItems.value) {
Expand Down Expand Up @@ -311,7 +316,7 @@ fun PdfViewer(
modifier = Modifier
.fillMaxSize()
.dragHandler(
key = uri,
key = key,
lazyGridState = state,
isVertical = true,
haptics = LocalHapticFeedback.current,
Expand Down Expand Up @@ -365,7 +370,7 @@ fun PdfViewer(
modifier = Modifier
.fillMaxSize()
.dragHandler(
key = uri,
key = key,
lazyGridState = state,
isVertical = false,
haptics = LocalHapticFeedback.current,
Expand All @@ -374,12 +379,12 @@ fun PdfViewer(
autoScrollThreshold = with(LocalDensity.current) { 40.dp.toPx() }
),
verticalArrangement = Arrangement.spacedBy(
spacing,
Alignment.CenterVertically
space = spacing,
alignment = Alignment.CenterVertically
),
horizontalArrangement = Arrangement.spacedBy(
spacing,
Alignment.CenterHorizontally
space = spacing,
alignment = Alignment.CenterHorizontally
),
contentPadding = PaddingValues(12.dp),
) {
Expand Down
Loading

0 comments on commit 1a750d8

Please sign in to comment.