diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..00212d8
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,25 @@
+# Third-party notices
+
+ClearPDF uses open-source components and keeps their notices with the project. Release packaging must preserve the applicable copyright and license text.
+
+## Apache PDFBox Android
+
+- Artifact: `com.tom-roush:pdfbox-android:2.0.27.0`
+- License: Apache License 2.0
+- Use: PDF parsing, editing, merging, splitting, and password removal.
+- Notice practice: keep the Apache 2.0 license and upstream attribution available with redistributed builds. Do not imply that the Apache Software Foundation endorses ClearPDF.
+
+## Apache POI
+
+- Artifacts: `org.apache.poi:poi:3.17`, `org.apache.poi:poi-scratchpad:3.17`
+- License: Apache License 2.0
+- Use: text extraction from legacy `.doc`, `.xls`, and `.ppt` files. Modern Office Open XML files use the platform ZIP/XML parser for a smaller footprint.
+- Notice practice: retain the Apache 2.0 license and any bundled dependency notices when distributing an APK or source package.
+
+## AndroidLiquidGlass / Backdrop
+
+- Component: `AndroidLiquidGlass` / the local `backdrop` module
+- License: Apache License 2.0 (as attributed in the Settings screen)
+- Use: the app's translucent glass surfaces, backdrop effects, and shared UI components.
+
+The app does not add GPL or LGPL components for document rendering. If a future dependency changes that, its license and redistribution obligations must be reviewed before release.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1dec36b..e57b729 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -40,7 +40,9 @@ android {
targetSdk = 36
versionCode = 2
versionName = "1.1.0"
- androidResources.localeFilters += arrayOf("en")
+ // Keep every locale declared by the app. Filtering this to English
+ // removes values-pt-rBR from the packaged APK, so the language picker
+ // can appear to work while the app continues to resolve English.
}
signingConfigs {
@@ -126,6 +128,10 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(project(":backdrop"))
implementation(project(":pdf-core"))
+ // Apache POI provides legacy .doc/.xls/.ppt text extraction. It is Apache-2.0
+ // licensed; see THIRD_PARTY_NOTICES.md for redistribution requirements.
+ implementation("org.apache.poi:poi:3.17")
+ implementation("org.apache.poi:poi-scratchpad:3.17")
// ML Kit Document Scanner & Camera
implementation(libs.play.services.mlkit.scanner)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 856506c..47b7fe2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -100,6 +100,13 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/chethan616/clearpdf/MainActivity.kt b/app/src/main/java/com/chethan616/clearpdf/MainActivity.kt
index b89ab46..207fa40 100644
--- a/app/src/main/java/com/chethan616/clearpdf/MainActivity.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/MainActivity.kt
@@ -45,7 +45,8 @@ class MainActivity : ComponentActivity() {
}
}
- // Handle PDF file opened from other apps (VIEW/SEND)
+ // Handle supported documents opened from other apps (VIEW/SEND).
+ // The viewer converts office/text/image sources into a local PDF preview.
val incomingPdfUri: Uri? = when (intent?.action) {
Intent.ACTION_VIEW -> intent.data
Intent.ACTION_SEND -> {
@@ -57,7 +58,7 @@ class MainActivity : ComponentActivity() {
}
}
else -> null
- }?.takeIf { intent?.type == "application/pdf" || intent?.data?.toString()?.endsWith(".pdf") == true }
+ }?.takeIf { uri -> isSupportedDocumentIntent(intent?.type, uri.toString()) }
// If a PDF was shared/opened, navigate to viewer with that URI
val effectiveRoute = if (incomingPdfUri != null) "pdf_viewer" else shortcutRoute
@@ -67,6 +68,23 @@ class MainActivity : ComponentActivity() {
}
}
+ private fun isSupportedDocumentIntent(mimeType: String?, uriString: String): Boolean {
+ val lower = uriString.lowercase()
+ val supportedExtension = listOf(
+ ".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".csv", ".txt", ".rtf",
+ ".odt", ".ods", ".odp", ".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".heic"
+ ).any(lower::contains)
+ return mimeType == null ||
+ mimeType == "application/pdf" ||
+ mimeType?.startsWith("image/") == true ||
+ mimeType?.startsWith("text/") == true ||
+ mimeType?.contains("word", ignoreCase = true) == true ||
+ mimeType?.contains("excel", ignoreCase = true) == true ||
+ mimeType?.contains("spreadsheet", ignoreCase = true) == true ||
+ mimeType?.contains("presentation", ignoreCase = true) == true ||
+ supportedExtension
+ }
+
private fun requestHighRefreshRate() {
// Prefer the display mode with the highest refresh rate
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
diff --git a/app/src/main/java/com/chethan616/clearpdf/data/repository/SignatureManager.kt b/app/src/main/java/com/chethan616/clearpdf/data/repository/SignatureManager.kt
index 66abb75..66c07f3 100644
--- a/app/src/main/java/com/chethan616/clearpdf/data/repository/SignatureManager.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/data/repository/SignatureManager.kt
@@ -25,9 +25,17 @@ object SignatureManager {
?.sortedByDescending { it.lastModified() }
?: emptyList()
- /** Saves a transparent-background signature bitmap; returns the saved file. */
- fun saveSignature(context: Context, bitmap: Bitmap): File {
- val file = File(dir(context), "sig_${System.currentTimeMillis()}.png")
+ /** Saves a transparent-background signature bitmap using a human-readable name. */
+ fun saveSignature(context: Context, bitmap: Bitmap, name: String = "Signature"): File {
+ val safeName = name
+ .trim()
+ .ifBlank { "Signature" }
+ .replace(Regex("[^\\p{L}\\p{N}\\-_ ]"), "")
+ .trim()
+ .replace(Regex("\\s+"), "_")
+ .take(48)
+ .ifBlank { "Signature" }
+ val file = File(dir(context), "${safeName}_${System.currentTimeMillis()}.png")
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
@@ -35,6 +43,19 @@ object SignatureManager {
return file
}
+ /** Returns the display name stored in a signature filename. */
+ fun displayName(file: File): String {
+ val base = file.nameWithoutExtension
+ val withoutTimestamp = base.substringBeforeLast('_').takeIf {
+ base.substringAfterLast('_').toLongOrNull() != null
+ } ?: base
+ return withoutTimestamp
+ .removePrefix("sig")
+ .replace('_', ' ')
+ .trim()
+ .ifBlank { "Signature" }
+ }
+
/** Loads a signature from file. Returns null if the file is missing or corrupt. */
fun loadSignature(file: File): Bitmap? = try {
val options = BitmapFactory.Options().apply {
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/DocsApp.kt b/app/src/main/java/com/chethan616/clearpdf/ui/DocsApp.kt
index 9f76ea5..4164919 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/DocsApp.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/DocsApp.kt
@@ -34,7 +34,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -67,6 +69,9 @@ import kotlinx.coroutines.flow.collectLatest
fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = null) {
val context = LocalContext.current
var selectedLocale by rememberSaveable { mutableStateOf(OnboardingManager.getSelectedLocale(context)) }
+ val localizedContext = remember(context, selectedLocale) {
+ com.chethan616.clearpdf.ui.utils.LocaleHelper.getLocalizedContext(context, selectedLocale)
+ }
var themeMode by rememberSaveable { mutableIntStateOf(AppSettingsManager.getThemeMode(context)) }
var showStarPrompt by rememberSaveable { mutableStateOf(false) }
@@ -154,13 +159,8 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
contentScale = ContentScale.Crop
)
- LaunchedEffect(selectedLocale) {
- if (selectedLocale.isNotBlank()) {
- com.chethan616.clearpdf.ui.utils.LocaleHelper.applyLocale(context, selectedLocale, recreate = false)
- }
- }
-
CompositionLocalProvider(
+ LocalResources provides localizedContext.resources,
LocalIsDarkMode provides isDarkMode
) {
Box(Modifier.fillMaxSize()) {
@@ -176,6 +176,19 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
themeMode = it
AppSettingsManager.setThemeMode(context, it)
},
+ selectedLocale = selectedLocale,
+ onLocaleChanged = { code ->
+ val normalized = com.chethan616.clearpdf.ui.utils.LocaleHelper.normalizeForUi(code)
+ if (normalized != selectedLocale) {
+ selectedLocale = normalized
+ com.chethan616.clearpdf.ui.utils.LocaleHelper.applyLocale(
+ context = context,
+ languageTag = normalized,
+ recreate = false,
+ updateAppCompat = false
+ )
+ }
+ },
incomingPdfUri = incomingPdfUri
)
@@ -247,11 +260,11 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
Modifier.size(52.dp), starAccent
)
BasicText(
- "Support ClearPDF",
+ stringResource(R.string.star_prompt_title),
style = TextStyle(starText, 20.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
BasicText(
- "ClearPDF is open source on GitHub.\nWould you like to star the project?",
+ stringResource(R.string.star_prompt_message),
style = TextStyle(starSub, 14.sp, textAlign = TextAlign.Center)
)
Spacer(Modifier.height(4.dp))
@@ -270,7 +283,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Star, null, Modifier.size(18.dp), Color.White)
- BasicText("Yes, Star It", style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.star_prompt_accept), style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
}
}
Row(
@@ -279,7 +292,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
verticalAlignment = Alignment.CenterVertically
) {
BasicText(
- "Not Now",
+ stringResource(R.string.star_prompt_later),
style = TextStyle(starSub, 13.sp, FontWeight.Medium, textAlign = TextAlign.Center),
modifier = Modifier
.clickable {
@@ -293,7 +306,7 @@ fun DocsApp(shortcutRoute: String? = null, incomingPdfUri: android.net.Uri? = nu
style = TextStyle(starSub.copy(0.4f), 13.sp)
)
BasicText(
- "Don't Ask Again",
+ stringResource(R.string.star_prompt_never),
style = TextStyle(starSub.copy(0.7f), 13.sp, FontWeight.Medium, textAlign = TextAlign.Center),
modifier = Modifier
.clickable {
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/components/DocsBottomTabs.kt b/app/src/main/java/com/chethan616/clearpdf/ui/components/DocsBottomTabs.kt
index dc8a076..e82f602 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/components/DocsBottomTabs.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/components/DocsBottomTabs.kt
@@ -7,11 +7,13 @@ import androidx.compose.material.icons.rounded.GridView
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.chethan616.clearpdf.ui.theme.LiquidGlassColors
import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
+import com.chethan616.clearpdf.R
import com.kyant.backdrop.Backdrop
@Composable
@@ -32,17 +34,17 @@ fun DocsBottomTabs(
modifier = modifier
) {
LiquidBottomTab(onClick = { onTabSelected(0) }) {
- Icon(Icons.Rounded.Home, contentDescription = "Home", tint = tint,
+ Icon(Icons.Rounded.Home, contentDescription = stringResource(R.string.nav_home), tint = tint,
modifier = Modifier.size(22.dp))
}
LiquidBottomTab(onClick = { onTabSelected(1) }) {
- Icon(Icons.Rounded.GridView, contentDescription = "Tools", tint = tint,
+ Icon(Icons.Rounded.GridView, contentDescription = stringResource(R.string.nav_tools), tint = tint,
modifier = Modifier.size(22.dp))
}
LiquidBottomTab(onClick = { onTabSelected(2) }) {
- Icon(Icons.Rounded.Tune, contentDescription = "Settings", tint = tint,
+ Icon(Icons.Rounded.Tune, contentDescription = stringResource(R.string.nav_settings), tint = tint,
modifier = Modifier.size(22.dp))
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidButton.kt b/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidButton.kt
index bb29e4a..c61e844 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidButton.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidButton.kt
@@ -34,6 +34,8 @@ import kotlin.math.tanh
import androidx.compose.material3.Icon
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathParser
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
private const val CLOSE_CROSS_SVG_PATH =
"M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"
@@ -55,7 +57,7 @@ fun CloseCrossIcon(modifier: Modifier = Modifier, tint: Color = Color.White) {
fill = androidx.compose.ui.graphics.SolidColor(tint)
).build()
}
- Icon(vector, contentDescription = "Close", modifier = modifier, tint = tint)
+ Icon(vector, contentDescription = stringResource(R.string.close), modifier = modifier, tint = tint)
}
/**
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidSaveDialog.kt b/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidSaveDialog.kt
index e69acc0..4773c55 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidSaveDialog.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/components/LiquidSaveDialog.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -26,6 +27,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
import com.chethan616.clearpdf.ui.utils.UISensor
import com.kyant.backdrop.Backdrop
@@ -59,7 +61,7 @@ fun LiquidSaveDialog(
context.contentResolver.takePersistableUriPermission(uri, flags)
} catch (_: Exception) {}
locationUri = uri
- locationDisplay = uri.lastPathSegment?.replace("primary:", "") ?: "Selected Folder"
+ locationDisplay = uri.lastPathSegment?.replace("primary:", "") ?: context.getString(R.string.selected_folder)
}
}
@@ -75,13 +77,13 @@ fun LiquidSaveDialog(
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
BasicText(
- "Save Document",
+ stringResource(R.string.save_document),
style = TextStyle(color = text, fontSize = 20.sp, fontWeight = FontWeight.SemiBold)
)
// File Name Input
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- BasicText("File Name", style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.file_name), style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
BasicTextField(
value = fileName,
onValueChange = { fileName = it },
@@ -94,7 +96,7 @@ fun LiquidSaveDialog(
.padding(14.dp),
decorationBox = { inner ->
if (fileName.isEmpty()) {
- BasicText("Document.pdf", style = TextStyle(color = sub.copy(0.5f), fontSize = 16.sp))
+ BasicText(stringResource(R.string.document_pdf), style = TextStyle(color = sub.copy(0.5f), fontSize = 16.sp))
}
inner()
}
@@ -103,7 +105,7 @@ fun LiquidSaveDialog(
// Save Location Picker
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- BasicText("Save Location", style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.settings_save_location), style = TextStyle(color = sub, fontSize = 13.sp, fontWeight = FontWeight.Medium))
LiquidButton(
onClick = { folderPicker.launch(null) },
backdrop = backdrop,
@@ -119,7 +121,7 @@ fun LiquidSaveDialog(
BasicText(locationDisplay, style = TextStyle(color = text, fontSize = 14.sp), maxLines = 1)
}
Spacer(Modifier.width(8.dp))
- Icon(Icons.Rounded.FolderOpen, "Change Folder", Modifier.size(20.dp), accent)
+ Icon(Icons.Rounded.FolderOpen, stringResource(R.string.change_folder), Modifier.size(20.dp), accent)
}
}
}
@@ -135,7 +137,7 @@ fun LiquidSaveDialog(
backdrop = backdrop,
surfaceColor = Color.Transparent
) {
- BasicText("Cancel", style = TextStyle(color = sub, fontSize = 14.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.cancel), style = TextStyle(color = sub, fontSize = 14.sp, fontWeight = FontWeight.Medium))
}
Spacer(Modifier.width(8.dp))
LiquidButton(
@@ -147,8 +149,8 @@ fun LiquidSaveDialog(
tint = accent
) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Rounded.Save, "Save", Modifier.size(16.dp), Color.White)
- BasicText("Save", style = TextStyle(color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold))
+ Icon(Icons.Rounded.Save, stringResource(R.string.save), Modifier.size(16.dp), Color.White)
+ BasicText(stringResource(R.string.save), style = TextStyle(color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/navigation/DocsNavGraph.kt b/app/src/main/java/com/chethan616/clearpdf/ui/navigation/DocsNavGraph.kt
index 073a5d8..6006e67 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/navigation/DocsNavGraph.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/navigation/DocsNavGraph.kt
@@ -23,6 +23,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import com.chethan616.clearpdf.ui.screen.CompressPdfScreen
import com.chethan616.clearpdf.ui.screen.CreatePdfScreen
+import com.chethan616.clearpdf.ui.screen.DecryptPdfScreen
+import com.chethan616.clearpdf.ui.screen.EncryptPdfScreen
import com.chethan616.clearpdf.ui.screen.ExtractTextScreen
import com.chethan616.clearpdf.ui.screen.ImagesToPdfScreen
import com.chethan616.clearpdf.ui.screen.HomeScreen
@@ -57,6 +59,8 @@ private const val ROUTE_CREATE = "create_pdf"
private const val ROUTE_ORGANIZE = "organize_pdf"
private const val ROUTE_EXTRACT_TEXT = "extract_text"
private const val ROUTE_IMAGES_TO_PDF = "images_to_pdf"
+private const val ROUTE_DECRYPT_PDF = "decrypt_pdf"
+private const val ROUTE_ENCRYPT_PDF = "encrypt_pdf"
private const val ROUTE_ONBOARDING = "onboarding"
private const val ARG_PDF_URI = "uri"
private const val ROUTE_VIEWER = "$ROUTE_VIEWER_BASE?$ARG_PDF_URI={$ARG_PDF_URI}"
@@ -100,6 +104,8 @@ fun DocsNavGraph(
onDarkModeChanged: (Boolean) -> Unit,
themeMode: Int,
onThemeModeChanged: (Int) -> Unit,
+ selectedLocale: String,
+ onLocaleChanged: (String) -> Unit,
incomingPdfUri: Uri? = null
) {
val context = LocalContext.current
@@ -194,6 +200,8 @@ fun DocsNavGraph(
composable(ROUTE_ONBOARDING) {
OnboardingScreen(
backdrop = backdrop,
+ selectedLocale = selectedLocale,
+ onLanguageChanged = onLocaleChanged,
onComplete = {
navController.navigate(ROUTE_HOME) {
popUpTo(ROUTE_ONBOARDING) { inclusive = true }
@@ -230,7 +238,9 @@ fun DocsNavGraph(
onNavigateToCreatePdf = { navController.navigate(ROUTE_CREATE) { launchSingleTop = true } },
onNavigateToOrganizePdf = { navController.navigate(ROUTE_ORGANIZE) { launchSingleTop = true } },
onNavigateToExtractText = { navController.navigate(ROUTE_EXTRACT_TEXT) { launchSingleTop = true } },
- onNavigateToImagesToPdf = { navController.navigate(ROUTE_IMAGES_TO_PDF) { launchSingleTop = true } }
+ onNavigateToImagesToPdf = { navController.navigate(ROUTE_IMAGES_TO_PDF) { launchSingleTop = true } },
+ onNavigateToDecryptPdf = { navController.navigate(ROUTE_DECRYPT_PDF) { launchSingleTop = true } },
+ onNavigateToEncryptPdf = { navController.navigate(ROUTE_ENCRYPT_PDF) { launchSingleTop = true } }
)
}
@@ -240,7 +250,9 @@ fun DocsNavGraph(
isDarkMode = isDarkMode,
onDarkModeChanged = onDarkModeChanged,
themeMode = themeMode,
- onThemeModeChanged = onThemeModeChanged
+ onThemeModeChanged = onThemeModeChanged,
+ selectedLocale = selectedLocale,
+ onLocaleChanged = onLocaleChanged
)
}
@@ -255,6 +267,22 @@ fun DocsNavGraph(
)
}
+ composable(ROUTE_DECRYPT_PDF) {
+ DecryptPdfScreen(
+ backdrop = backdrop,
+ onBack = { navController.popBackStack() },
+ onViewOutput = { uri -> navController.navigateToPdfViewer(uri) }
+ )
+ }
+
+ composable(ROUTE_ENCRYPT_PDF) {
+ EncryptPdfScreen(
+ backdrop = backdrop,
+ onBack = { navController.popBackStack() },
+ onViewOutput = { uri -> navController.navigateToPdfViewer(uri) }
+ )
+ }
+
composable(
route = ROUTE_VIEWER,
arguments = listOf(
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/CompressPdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/CompressPdfScreen.kt
index a6876b5..056d2aa 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/CompressPdfScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/CompressPdfScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -126,9 +129,9 @@ fun CompressPdfScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar(title = "Compress PDF", backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f), titleFontSize = 18.sp)
+ LiquidGlassTopBar(title = stringResource(R.string.tool_compress), backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f), titleFontSize = 18.sp)
}
// Scrollable Body Content
@@ -152,8 +155,8 @@ fun CompressPdfScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(Icons.Rounded.Compress, null, Modifier.size(56.dp), accent)
- BasicText("Reduce File Size", style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
- BasicText("Compress your PDF while preserving quality", style = TextStyle(sub, 14.sp, textAlign = TextAlign.Center))
+ BasicText(stringResource(R.string.tool_compress), style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.tool_compress_sub), style = TextStyle(sub, 14.sp, textAlign = TextAlign.Center))
LiquidButton(
onClick = { filePicker.launch(arrayOf("application/pdf")) },
@@ -161,7 +164,7 @@ fun CompressPdfScreen(
) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
- BasicText("Select PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_pick_pdf), style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
}
}
}
@@ -176,7 +179,7 @@ fun CompressPdfScreen(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
BasicText(state.sourceFileName, style = TextStyle(text, 15.sp, fontWeight = FontWeight.Medium))
- BasicText("Size: ${state.originalSizeBytes / 1024} KB", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.size_kb, state.originalSizeBytes / 1024), style = TextStyle(sub, 13.sp))
}
// Un-nested slider control for maximum performance (no glass card lag)
@@ -196,7 +199,7 @@ fun CompressPdfScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Rounded.HighQuality, null, Modifier.size(22.dp), accent)
- BasicText("Compression Quality", style = TextStyle(text, 16.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_compression_quality), style = TextStyle(text, 16.sp, fontWeight = FontWeight.SemiBold))
}
Box(
Modifier
@@ -223,8 +226,8 @@ fun CompressPdfScreen(
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- BasicText("Smaller file", style = TextStyle(sub, 12.sp))
- BasicText("Better quality", style = TextStyle(sub, 12.sp))
+ BasicText(stringResource(R.string.settings_smaller_size), style = TextStyle(sub, 12.sp))
+ BasicText(stringResource(R.string.settings_higher_quality), style = TextStyle(sub, 12.sp))
}
if (state.estimatedSizeBytes > 0) {
@@ -273,7 +276,7 @@ fun CompressPdfScreen(
tint = Color(0xFF1976D2),
modifier = Modifier.fillMaxWidth()
) {
- BasicText("View PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.viewer_open_pdf), style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/CreatePdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/CreatePdfScreen.kt
index cd30000..b52f0ee 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/CreatePdfScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/CreatePdfScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import android.app.Activity
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -122,7 +125,7 @@ fun CreatePdfScreen(
fun launchScanner() {
if (activity == null) {
- viewModel.setError("Scanner is unavailable in current context")
+ viewModel.setError(context.getString(R.string.scanner_unavailable))
return
}
val options = GmsDocumentScannerOptions.Builder()
@@ -138,7 +141,7 @@ fun CreatePdfScreen(
scannerLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
.addOnFailureListener { error ->
- viewModel.setError("Could not start scanner: ${error.message}")
+ viewModel.setError(context.getString(R.string.scanner_start_failed))
}
}
@@ -193,9 +196,9 @@ fun CreatePdfScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar(title = "Create PDF", backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f))
+ LiquidGlassTopBar(title = stringResource(R.string.tool_create), backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f))
}
Column(
@@ -217,7 +220,7 @@ fun CreatePdfScreen(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Icon(Icons.AutoMirrored.Rounded.NoteAdd, null, Modifier.size(52.dp), accent)
- BasicText("Create with scans, images, or blank pages", style = TextStyle(text, 19.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.create_pdf_subtitle), style = TextStyle(text, 19.sp, fontWeight = FontWeight.SemiBold))
BasicText(
"Image-first creation is now default. Text is available as an advanced mode.",
style = TextStyle(sub, 13.sp, textAlign = TextAlign.Center)
@@ -232,9 +235,9 @@ fun CreatePdfScreen(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val templates = listOf(
- Triple(CreateMode.FROM_IMAGES, "Import Images", Icons.Rounded.Image),
- Triple(CreateMode.BLANK, "Blank Pages", Icons.Rounded.Description),
- Triple(CreateMode.ADVANCED_TEXT, "Advanced Text", Icons.Rounded.Edit)
+ Triple(CreateMode.FROM_IMAGES, stringResource(R.string.create_mode_images), Icons.Rounded.Image),
+ Triple(CreateMode.BLANK, stringResource(R.string.create_mode_blank), Icons.Rounded.Description),
+ Triple(CreateMode.ADVANCED_TEXT, stringResource(R.string.create_mode_text), Icons.Rounded.Edit)
)
templates.forEach { (mode, label, icon) ->
@@ -291,7 +294,7 @@ fun CreatePdfScreen(
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(Icons.Rounded.CameraAlt, null, Modifier.size(17.dp), Color.White)
- BasicText("Scan", style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.home_scan), style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
}
}
LiquidButton(
@@ -305,7 +308,7 @@ fun CreatePdfScreen(
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(Icons.Rounded.UploadFile, null, Modifier.size(17.dp), text)
- BasicText("Add Images", style = TextStyle(text, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.images_add), style = TextStyle(text, 14.sp, FontWeight.Medium))
}
}
}
@@ -318,7 +321,7 @@ fun CreatePdfScreen(
.liquidGlassPanel(backdrop, uiSensor)
.padding(16.dp)
) {
- BasicText("Scan or add images to start your PDF draft.", style = TextStyle(sub, 14.sp))
+ BasicText(stringResource(R.string.create_scan_or_images_hint), style = TextStyle(sub, 14.sp))
}
} else {
LazyColumn(
@@ -338,7 +341,7 @@ fun CreatePdfScreen(
) {
Image(
painter = rememberAsyncImagePainter(uri),
- contentDescription = "Page ${index + 1}",
+ contentDescription = stringResource(R.string.page_number, index + 1),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = 62.dp, height = 82.dp)
@@ -346,8 +349,8 @@ fun CreatePdfScreen(
)
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
- BasicText("Page ${index + 1}", style = TextStyle(text, 14.sp, FontWeight.SemiBold))
- BasicText("Tap arrows to reorder", style = TextStyle(sub, 12.sp))
+ BasicText(stringResource(R.string.create_page_label, index + 1), style = TextStyle(text, 14.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.create_reorder_hint), style = TextStyle(sub, 12.sp))
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
@@ -401,8 +404,8 @@ fun CreatePdfScreen(
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
- BasicText("Create a clean blank PDF", style = TextStyle(text, 16.sp, FontWeight.SemiBold))
- BasicText("Set the number of pages and generate instantly.", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.create_blank_title), style = TextStyle(text, 16.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.create_blank_subtitle), style = TextStyle(sub, 13.sp))
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -411,7 +414,7 @@ fun CreatePdfScreen(
LiquidButton(onClick = { viewModel.onBlankPageCountChanged(state.blankPageCount - 1) }, backdrop = backdrop) {
BasicText("-", style = TextStyle(text, 18.sp, FontWeight.Bold))
}
- BasicText("${state.blankPageCount} pages", style = TextStyle(text, 18.sp, FontWeight.Bold))
+ BasicText(stringResource(R.string.create_pages, state.blankPageCount), style = TextStyle(text, 18.sp, FontWeight.Bold))
LiquidButton(onClick = { viewModel.onBlankPageCountChanged(state.blankPageCount + 1) }, backdrop = backdrop, tint = accent) {
BasicText("+", style = TextStyle(Color.White, 18.sp, FontWeight.Bold))
}
@@ -428,8 +431,8 @@ fun CreatePdfScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- BasicText("Advanced text to PDF", style = TextStyle(text, 16.sp, FontWeight.SemiBold))
- BasicText("Use this for quick notes. For polished docs, prefer scan/image mode.", style = TextStyle(sub, 12.sp))
+ BasicText(stringResource(R.string.create_text_title), style = TextStyle(text, 16.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.create_text_subtitle), style = TextStyle(sub, 12.sp))
BasicTextField(
value = state.textContent,
onValueChange = { viewModel.onTextChanged(it) },
@@ -443,20 +446,20 @@ fun CreatePdfScreen(
.padding(12.dp),
decorationBox = { inner ->
if (state.textContent.isEmpty()) {
- BasicText("Type text content...", style = TextStyle(sub.copy(alpha = 0.55f), 14.sp))
+ BasicText(stringResource(R.string.create_text_hint), style = TextStyle(sub.copy(alpha = 0.55f), 14.sp))
}
inner()
}
)
- BasicText("${state.textContent.length} characters", style = TextStyle(sub, 12.sp))
+ BasicText(stringResource(R.string.create_character_count, state.textContent.length), style = TextStyle(sub, 12.sp))
}
}
}
val createLabel = when (state.selectedMode) {
- CreateMode.FROM_IMAGES -> "Create PDF from Draft"
- CreateMode.BLANK -> "Create Blank PDF"
- CreateMode.ADVANCED_TEXT -> "Create Text PDF"
+ CreateMode.FROM_IMAGES -> stringResource(R.string.create_from_draft)
+ CreateMode.BLANK -> stringResource(R.string.create_blank_pdf)
+ CreateMode.ADVANCED_TEXT -> stringResource(R.string.create_text_pdf)
}
LiquidButton(
@@ -501,7 +504,7 @@ fun CreatePdfScreen(
tint = Color(0xFF1976D2),
modifier = Modifier.fillMaxWidth()
) {
- BasicText("View PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.viewer_open_pdf), style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/DecryptPdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/DecryptPdfScreen.kt
new file mode 100644
index 0000000..2f34c50
--- /dev/null
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/DecryptPdfScreen.kt
@@ -0,0 +1,210 @@
+package com.chethan616.clearpdf.ui.screen
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBackIosNew
+import androidx.compose.material.icons.rounded.LockOpen
+import androidx.compose.material.icons.rounded.OpenInNew
+import androidx.compose.material.icons.rounded.UploadFile
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.chethan616.clearpdf.R
+import com.chethan616.clearpdf.ui.components.LiquidButton
+import com.chethan616.clearpdf.ui.components.LiquidGlassTopBar
+import com.chethan616.clearpdf.ui.components.liquidGlassPanel
+import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
+import com.chethan616.clearpdf.ui.utils.rememberUISensor
+import com.kyant.backdrop.backdrops.LayerBackdrop
+import com.kyant.pdfcore.security.PdfSecurityService
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@Composable
+fun DecryptPdfScreen(
+ backdrop: LayerBackdrop,
+ onBack: () -> Unit,
+ onViewOutput: (android.net.Uri) -> Unit
+) {
+ val context = LocalContext.current
+ val isDark = LocalIsDarkMode.current
+ val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF222222)
+ val sub = if (isDark) Color(0xFFAAAAAA) else Color(0xFF777777)
+ val accent = Color(0xFF7C4DFF)
+ val uiSensor = rememberUISensor()
+ val scope = rememberCoroutineScope()
+ var sourceUri by remember { mutableStateOf(null) }
+ var outputUri by remember { mutableStateOf(null) }
+ var password by remember { mutableStateOf("") }
+ var isBusy by remember { mutableStateOf(false) }
+ var message by remember { mutableStateOf(null) }
+ var isError by remember { mutableStateOf(false) }
+
+ val saveLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.CreateDocument("application/pdf")
+ ) { destination ->
+ val source = sourceUri ?: return@rememberLauncherForActivityResult
+ if (destination == null) return@rememberLauncherForActivityResult
+ isBusy = true
+ message = null
+ scope.launch {
+ try {
+ withContext(Dispatchers.IO) {
+ PdfSecurityService.decryptToUri(context, source, destination, password)
+ }
+ isError = false
+ outputUri = destination
+ message = context.getString(R.string.decrypt_pdf_success)
+ } catch (_: PdfSecurityService.PasswordRequiredException) {
+ isError = true
+ message = context.getString(R.string.decrypt_pdf_invalid)
+ } catch (_: Exception) {
+ isError = true
+ message = context.getString(R.string.file_preview_unsupported)
+ } finally {
+ isBusy = false
+ }
+ }
+ }
+
+ val openLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
+ sourceUri = uri
+ outputUri = null
+ message = null
+ password = ""
+ }
+
+ Column(
+ Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
+ }
+ LiquidGlassTopBar(stringResource(R.string.decrypt_pdf_title), backdrop, uiSensor, Modifier.weight(1f))
+ }
+ Column(
+ Modifier.fillMaxWidth().liquidGlassPanel(backdrop, uiSensor).padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ Icon(Icons.Rounded.LockOpen, null, Modifier.size(52.dp), accent)
+ BasicText(stringResource(R.string.decrypt_pdf_title), style = TextStyle(text, 21.sp, FontWeight.SemiBold))
+ BasicText(
+ stringResource(R.string.decrypt_pdf_subtitle),
+ style = TextStyle(sub, 14.sp),
+ modifier = Modifier.padding(horizontal = 10.dp)
+ )
+ LiquidButton(
+ onClick = { openLauncher.launch(arrayOf("application/pdf")) },
+ backdrop = backdrop,
+ tint = accent,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
+ BasicText(stringResource(R.string.decrypt_pdf_select), style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
+ }
+
+ sourceUri?.let { source ->
+ BasicText(
+ source.lastPathSegment ?: stringResource(R.string.viewer_title),
+ style = TextStyle(text, 14.sp, FontWeight.Medium),
+ maxLines = 1
+ )
+ LiquidButton(
+ onClick = { onViewOutput(source) },
+ backdrop = backdrop,
+ surfaceColor = Color.White.copy(0.08f),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Rounded.OpenInNew, null, Modifier.size(17.dp), accent)
+ BasicText(stringResource(R.string.view_pdf), style = TextStyle(text, 14.sp, FontWeight.Medium))
+ }
+ BasicTextField(
+ value = password,
+ onValueChange = { password = it },
+ singleLine = true,
+ textStyle = TextStyle(text, 15.sp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(if (isDark) Color.White.copy(0.08f) else Color.Black.copy(0.05f))
+ .padding(horizontal = 12.dp, vertical = 12.dp),
+ decorationBox = { inner ->
+ if (password.isEmpty()) BasicText(stringResource(R.string.decrypt_pdf_password), style = TextStyle(sub, 14.sp))
+ inner()
+ }
+ )
+ LiquidButton(
+ onClick = { saveLauncher.launch("unlocked.pdf") },
+ backdrop = backdrop,
+ tint = accent,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ if (isBusy) CircularProgressIndicator(Modifier.size(18.dp), color = Color.White, strokeWidth = 2.dp)
+ else BasicText(stringResource(R.string.decrypt_pdf_output), style = TextStyle(Color.White, 14.sp, FontWeight.SemiBold))
+ }
+ }
+
+ AnimatedVisibility(visible = message != null, enter = fadeIn(), exit = fadeOut()) {
+ message?.let { BasicText(it, style = TextStyle(if (isError) Color(0xFFD32F2F) else Color(0xFF2E7D32), 13.sp)) }
+ }
+ outputUri?.let { result ->
+ LiquidButton(
+ onClick = { onViewOutput(result) },
+ backdrop = backdrop,
+ tint = Color(0xFF2E7D32),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Rounded.OpenInNew, null, Modifier.size(17.dp), Color.White)
+ BasicText(stringResource(R.string.view_unlocked_pdf), style = TextStyle(Color.White, 14.sp, FontWeight.SemiBold))
+ }
+ }
+ }
+ androidx.compose.foundation.layout.Spacer(
+ Modifier.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 68.dp)
+ )
+ }
+}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/EncryptPdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/EncryptPdfScreen.kt
new file mode 100644
index 0000000..930ec5b
--- /dev/null
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/EncryptPdfScreen.kt
@@ -0,0 +1,268 @@
+package com.chethan616.clearpdf.ui.screen
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBackIosNew
+import androidx.compose.material.icons.rounded.Lock
+import androidx.compose.material.icons.rounded.OpenInNew
+import androidx.compose.material.icons.rounded.UploadFile
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.chethan616.clearpdf.R
+import com.chethan616.clearpdf.ui.components.LiquidButton
+import com.chethan616.clearpdf.ui.components.LiquidGlassTopBar
+import com.chethan616.clearpdf.ui.components.liquidGlassPanel
+import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
+import com.chethan616.clearpdf.ui.utils.rememberUISensor
+import com.kyant.backdrop.backdrops.LayerBackdrop
+import com.kyant.pdfcore.security.PdfSecurityService
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@Composable
+fun EncryptPdfScreen(
+ backdrop: LayerBackdrop,
+ onBack: () -> Unit,
+ onViewOutput: (android.net.Uri) -> Unit
+) {
+ val context = LocalContext.current
+ val isDark = LocalIsDarkMode.current
+ val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF222222)
+ val sub = if (isDark) Color(0xFFAAAAAA) else Color(0xFF777777)
+ val accent = Color(0xFF5E35B1)
+ val uiSensor = rememberUISensor()
+ val scope = rememberCoroutineScope()
+ var sourceUri by remember { mutableStateOf(null) }
+ var outputUri by remember { mutableStateOf(null) }
+ var password by remember { mutableStateOf("") }
+ var confirmPassword by remember { mutableStateOf("") }
+ var isBusy by remember { mutableStateOf(false) }
+ var message by remember { mutableStateOf(null) }
+ var isError by remember { mutableStateOf(false) }
+
+ val saveLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.CreateDocument("application/pdf")
+ ) { destination ->
+ val source = sourceUri ?: return@rememberLauncherForActivityResult
+ if (destination == null) return@rememberLauncherForActivityResult
+ if (password.isBlank()) {
+ isError = true
+ message = context.getString(R.string.encrypt_pdf_password_required)
+ return@rememberLauncherForActivityResult
+ }
+ if (password != confirmPassword) {
+ isError = true
+ message = context.getString(R.string.encrypt_pdf_mismatch)
+ return@rememberLauncherForActivityResult
+ }
+ isBusy = true
+ message = null
+ scope.launch {
+ try {
+ withContext(Dispatchers.IO) {
+ PdfSecurityService.encryptToUri(context, source, destination, password)
+ }
+ outputUri = destination
+ isError = false
+ message = context.getString(R.string.encrypt_pdf_success)
+ } catch (_: PdfSecurityService.PasswordRequiredException) {
+ isError = true
+ message = context.getString(R.string.encrypt_pdf_source_protected)
+ } catch (_: Exception) {
+ isError = true
+ message = context.getString(R.string.file_preview_unsupported)
+ } finally {
+ isBusy = false
+ }
+ }
+ }
+
+ val openLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
+ sourceUri = uri
+ outputUri = null
+ message = null
+ password = ""
+ confirmPassword = ""
+ }
+
+ Column(
+ Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
+ }
+ LiquidGlassTopBar(stringResource(R.string.encrypt_pdf_title), backdrop, uiSensor, Modifier.weight(1f))
+ }
+
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .liquidGlassPanel(backdrop, uiSensor)
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ Icon(Icons.Rounded.Lock, null, Modifier.size(52.dp), accent)
+ BasicText(stringResource(R.string.encrypt_pdf_title), style = TextStyle(text, 21.sp, FontWeight.SemiBold))
+ BasicText(
+ stringResource(R.string.encrypt_pdf_subtitle),
+ style = TextStyle(sub, 14.sp),
+ modifier = Modifier.padding(horizontal = 10.dp)
+ )
+ LiquidButton(
+ onClick = { openLauncher.launch(arrayOf("application/pdf")) },
+ backdrop = backdrop,
+ tint = accent,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
+ BasicText(stringResource(R.string.encrypt_pdf_select), style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
+ }
+
+ sourceUri?.let { source ->
+ BasicText(
+ source.lastPathSegment ?: stringResource(R.string.viewer_title),
+ style = TextStyle(text, 14.sp, FontWeight.Medium),
+ maxLines = 1
+ )
+ LiquidButton(
+ onClick = { onViewOutput(source) },
+ backdrop = backdrop,
+ surfaceColor = Color.White.copy(0.08f),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Rounded.OpenInNew, null, Modifier.size(17.dp), accent)
+ BasicText(stringResource(R.string.view_pdf), style = TextStyle(text, 14.sp, FontWeight.Medium))
+ }
+ SecureInput(
+ value = password,
+ placeholder = stringResource(R.string.encrypt_pdf_password),
+ text = text,
+ sub = sub,
+ isDark = isDark,
+ onValueChange = { password = it }
+ )
+ SecureInput(
+ value = confirmPassword,
+ placeholder = stringResource(R.string.encrypt_pdf_confirm),
+ text = text,
+ sub = sub,
+ isDark = isDark,
+ onValueChange = { confirmPassword = it }
+ )
+ LiquidButton(
+ onClick = {
+ when {
+ password.isBlank() -> {
+ isError = true
+ message = context.getString(R.string.encrypt_pdf_password_required)
+ }
+ password != confirmPassword -> {
+ isError = true
+ message = context.getString(R.string.encrypt_pdf_mismatch)
+ }
+ else -> saveLauncher.launch("encrypted.pdf")
+ }
+ },
+ backdrop = backdrop,
+ tint = if (password.isNotBlank() && password == confirmPassword) accent else Color.White.copy(0.10f),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ if (isBusy) CircularProgressIndicator(Modifier.size(18.dp), color = Color.White, strokeWidth = 2.dp)
+ else BasicText(stringResource(R.string.encrypt_pdf_output), style = TextStyle(Color.White, 14.sp, FontWeight.SemiBold))
+ }
+ }
+
+ AnimatedVisibility(visible = message != null, enter = fadeIn(), exit = fadeOut()) {
+ message?.let { BasicText(it, style = TextStyle(if (isError) Color(0xFFD32F2F) else Color(0xFF2E7D32), 13.sp)) }
+ }
+
+ outputUri?.let { result ->
+ LiquidButton(
+ onClick = { onViewOutput(result) },
+ backdrop = backdrop,
+ tint = Color(0xFF2E7D32),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Rounded.OpenInNew, null, Modifier.size(17.dp), Color.White)
+ BasicText(stringResource(R.string.view_encrypted_pdf), style = TextStyle(Color.White, 14.sp, FontWeight.SemiBold))
+ }
+ }
+ }
+ androidx.compose.foundation.layout.Spacer(
+ Modifier.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 68.dp)
+ )
+ }
+}
+
+@Composable
+private fun SecureInput(
+ value: String,
+ placeholder: String,
+ text: Color,
+ sub: Color,
+ isDark: Boolean,
+ onValueChange: (String) -> Unit
+) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ singleLine = true,
+ textStyle = TextStyle(text, 15.sp),
+ visualTransformation = PasswordVisualTransformation(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(if (isDark) Color.White.copy(0.08f) else Color.Black.copy(0.05f))
+ .padding(horizontal = 12.dp, vertical = 12.dp),
+ decorationBox = { inner ->
+ if (value.isEmpty()) BasicText(placeholder, style = TextStyle(sub, 14.sp))
+ inner()
+ }
+ )
+}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ExtractTextScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ExtractTextScreen.kt
index 0e535cf..ab14c86 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ExtractTextScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ExtractTextScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -110,9 +113,9 @@ fun ExtractTextScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar("Extract Text", backdrop, uiSensor, Modifier.weight(1f), titleFontSize = 18.sp)
+ LiquidGlassTopBar(stringResource(R.string.tool_extract), backdrop, uiSensor, Modifier.weight(1f), titleFontSize = 18.sp)
}
Column(
@@ -137,12 +140,12 @@ fun ExtractTextScreen(
modifier = Modifier.weight(1f)
) {
Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
- BasicText("Pick a PDF", style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_pick_pdf), style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
}
if (state.text.isNotEmpty()) {
LiquidButton(onClick = { clipboard.setText(AnnotatedString(state.text)) }, backdrop = backdrop) {
Icon(Icons.Rounded.ContentCopy, null, Modifier.size(16.dp), text)
- BasicText("Copy", style = TextStyle(text, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.copy), style = TextStyle(text, 14.sp, FontWeight.Medium))
}
com.chethan616.clearpdf.ui.components.LiquidIconButton(
onClick = {
@@ -150,13 +153,13 @@ fun ExtractTextScreen(
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, state.text)
}
- context.startActivity(Intent.createChooser(send, "Share text"))
+ context.startActivity(Intent.createChooser(send, context.getString(R.string.extract_share_text)))
},
backdrop = backdrop,
tint = Color(0xFF0088FF),
modifier = Modifier.size(44.dp)
) {
- Icon(Icons.Rounded.Share, "Share", Modifier.size(20.dp), Color.White)
+ Icon(Icons.Rounded.Share, stringResource(R.string.share), Modifier.size(20.dp), Color.White)
}
}
}
@@ -175,10 +178,10 @@ fun ExtractTextScreen(
BasicText(state.text, style = TextStyle(text, 14.sp))
}
state.hasResult -> Box(Modifier.fillMaxSize(), Alignment.Center) {
- BasicText("No text layer in this PDF.", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.extract_no_text), style = TextStyle(sub, 13.sp))
}
else -> Box(Modifier.fillMaxSize(), Alignment.Center) {
- BasicText("Pick a PDF to pull its text.", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.extract_pick_hint), style = TextStyle(sub, 13.sp))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/HomeScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/HomeScreen.kt
index 0cb170d..d875586 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/HomeScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/HomeScreen.kt
@@ -4,7 +4,10 @@ import android.content.Intent
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
@@ -59,7 +62,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathParser
@@ -119,48 +121,32 @@ fun HomeScreen(
var infoRecent by remember { mutableStateOf(null) }
val lifecycleOwner = LocalLifecycleOwner.current
- var b1Visible by remember { mutableStateOf(false) }
- var b2Visible by remember { mutableStateOf(false) }
- var b3Visible by remember { mutableStateOf(false) }
- var b4Visible by remember { mutableStateOf(false) }
-
- val morphProgress by androidx.compose.animation.core.animateFloatAsState(
- targetValue = if (selectedRecent != null) 1f else 0f,
- animationSpec = spring(
- dampingRatio = 0.68f,
- stiffness = 180f
- ),
- label = "morphProgress"
+ val popupTransition = updateTransition(
+ targetState = selectedRecent != null,
+ label = "recentPopupTransition"
)
-
- androidx.compose.runtime.LaunchedEffect(selectedRecent) {
- if (selectedRecent != null) {
- b1Visible = false
- b2Visible = false
- b3Visible = false
- b4Visible = false
- // Wait for pop-up container to morph open gracefully (~220ms)
- kotlinx.coroutines.delay(220L)
- b1Visible = true
- kotlinx.coroutines.delay(65L)
- b2Visible = true
- kotlinx.coroutines.delay(65L)
- b3Visible = true
- kotlinx.coroutines.delay(65L)
- b4Visible = true
- } else {
- b1Visible = false
- b2Visible = false
- b3Visible = false
- b4Visible = false
- }
- }
-
- val btnSpring = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium)
- val b1Scale by androidx.compose.animation.core.animateFloatAsState(if (b1Visible) 1f else 0f, btnSpring, label = "b1")
- val b2Scale by androidx.compose.animation.core.animateFloatAsState(if (b2Visible) 1f else 0f, btnSpring, label = "b2")
- val b3Scale by androidx.compose.animation.core.animateFloatAsState(if (b3Visible) 1f else 0f, btnSpring, label = "b3")
- val b4Scale by androidx.compose.animation.core.animateFloatAsState(if (b4Visible) 1f else 0f, btnSpring, label = "b4")
+ val morphProgress by popupTransition.animateFloat(
+ transitionSpec = { spring(dampingRatio = 0.82f, stiffness = 220f) },
+ label = "morphProgress"
+ ) { if (it) 1f else 0f }
+ // One transition coordinates the buttons with the sheet. This removes four
+ // independent state machines and keeps the glass surface on a single frame clock.
+ val b1Scale by popupTransition.animateFloat(
+ transitionSpec = { tween(240, if (targetState) 110 else 0) },
+ label = "popupOpenScale"
+ ) { if (it) 1f else 0f }
+ val b2Scale by popupTransition.animateFloat(
+ transitionSpec = { tween(240, if (targetState) 145 else 0) },
+ label = "popupShareScale"
+ ) { if (it) 1f else 0f }
+ val b3Scale by popupTransition.animateFloat(
+ transitionSpec = { tween(240, if (targetState) 180 else 0) },
+ label = "popupDetailsScale"
+ ) { if (it) 1f else 0f }
+ val b4Scale by popupTransition.animateFloat(
+ transitionSpec = { tween(240, if (targetState) 215 else 0) },
+ label = "popupRemoveScale"
+ ) { if (it) 1f else 0f }
DisposableEffect(lifecycleOwner, context) {
val observer = LifecycleEventObserver { _, event ->
@@ -420,7 +406,7 @@ fun HomeScreen(
)
val timeStr = formatTimestamp(recent.timestamp)
val sizeStr = if (recent.sizeBytes > 0) " · ${formatFileSize(recent.sizeBytes)}" else ""
- val pageStr = if (recent.pageCount > 0) " · ${recent.pageCount} pg" else ""
+ val pageStr = if (recent.pageCount > 0) " · ${stringResource(R.string.recents_page_count, recent.pageCount)}" else ""
BasicText("$timeStr$sizeStr$pageStr", style = TextStyle(sub, 11.sp))
}
@@ -430,13 +416,13 @@ fun HomeScreen(
.background(accent.copy(0.12f))
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
- BasicText("PDF", style = TextStyle(accent, 9.sp, FontWeight.Bold))
+ BasicText(stringResource(R.string.recents_pdf_type), style = TextStyle(accent, 9.sp, FontWeight.Bold))
}
}
}
if (recents.size > homeRecentLimit && !showAllRecents) {
BasicText(
- "Long press for quick actions",
+ stringResource(R.string.recents_long_press_hint),
style = TextStyle(sub.copy(0.6f), 11.sp, textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
)
@@ -480,7 +466,6 @@ fun HomeScreen(
alpha = morphProgress.coerceIn(0f, 1f)
transformOrigin = TransformOrigin(0.5f, 0.85f)
shadowElevation = (16f * morphProgress).dp.toPx()
- compositingStrategy = CompositingStrategy.Offscreen
}
.liquidGlassPanel(backdrop, uiSensor)
.clickable(
@@ -559,7 +544,7 @@ fun HomeScreen(
) {
Icon(Icons.Rounded.FileOpen, null, Modifier.size(22.dp), Color.White)
}
- BasicText("Open", style = TextStyle(text, 11.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.recents_open), style = TextStyle(text, 11.sp, FontWeight.Medium))
}
// 2. Share Button (Green)
@@ -580,7 +565,7 @@ fun HomeScreen(
putExtra(Intent.EXTRA_STREAM, recent.uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
- context.startActivity(Intent.createChooser(shareIntent, "Share PDF"))
+ context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.recents_share_pdf)))
},
backdrop = backdrop,
tint = Color(0xFF4CAF50),
@@ -588,7 +573,7 @@ fun HomeScreen(
) {
Icon(Icons.Rounded.Share, null, Modifier.size(22.dp), Color.White)
}
- BasicText("Share", style = TextStyle(text, 11.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.recents_share), style = TextStyle(text, 11.sp, FontWeight.Medium))
}
// 3. Info Button (Purple)
@@ -612,7 +597,7 @@ fun HomeScreen(
) {
Icon(Icons.Rounded.Info, null, Modifier.size(22.dp), Color.White)
}
- BasicText("Details", style = TextStyle(text, 11.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.recents_details), style = TextStyle(text, 11.sp, FontWeight.Medium))
}
// 4. Remove Button (Red)
@@ -637,7 +622,7 @@ fun HomeScreen(
) {
CloseCrossIcon(Modifier.size(18.dp), Color.White)
}
- BasicText("Remove", style = TextStyle(redAccent, 11.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.recents_remove), style = TextStyle(redAccent, 11.sp, FontWeight.Medium))
}
}
}
@@ -685,7 +670,7 @@ fun HomeScreen(
}
Column(Modifier.weight(1f)) {
BasicText(
- "File Info",
+ stringResource(R.string.recents_file_info),
style = TextStyle(text, 18.sp, FontWeight.SemiBold)
)
BasicText(
@@ -704,10 +689,10 @@ fun HomeScreen(
.background(if (isLight) Color.Black.copy(0.06f) else Color.White.copy(0.08f))
)
- InfoRow("Pages", if (recent.pageCount > 0) recent.pageCount.toString() else "Unknown", text, sub)
- InfoRow("Size", if (recent.sizeBytes > 0) formatFileSize(recent.sizeBytes) else "Unknown", text, sub)
- InfoRow("Added", formatTimestamp(recent.timestamp), text, sub)
- InfoRow("Location", recent.uri.toString(), text, sub, maxLines = 3)
+ InfoRow(stringResource(R.string.recents_pages), if (recent.pageCount > 0) recent.pageCount.toString() else stringResource(R.string.recents_unknown), text, sub)
+ InfoRow(stringResource(R.string.recents_size_label), if (recent.sizeBytes > 0) formatFileSize(recent.sizeBytes) else stringResource(R.string.recents_unknown), text, sub)
+ InfoRow(stringResource(R.string.recents_added), formatTimestamp(recent.timestamp), text, sub)
+ InfoRow(stringResource(R.string.recents_location), recent.uri.toString(), text, sub, maxLines = 3)
LiquidButton(
onClick = { infoRecent = null },
@@ -715,7 +700,7 @@ fun HomeScreen(
tint = accent,
modifier = Modifier.fillMaxWidth()
) {
- BasicText("Done", style = TextStyle(Color.White, 14.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.done), style = TextStyle(Color.White, 14.sp, FontWeight.SemiBold))
}
}
}
@@ -765,20 +750,22 @@ private fun ActionSheetItem(
}
}
+@Composable
private fun formatTimestamp(ts: Long): String {
val now = System.currentTimeMillis()
val diff = now - ts
return when {
- diff < 60_000 -> "Just now"
- diff < 3_600_000 -> "${diff / 60_000} min ago"
- diff < 86_400_000 -> "${diff / 3_600_000}h ago"
- diff < 172_800_000 -> "Yesterday"
+ diff < 60_000 -> stringResource(R.string.recents_just_now)
+ diff < 3_600_000 -> stringResource(R.string.recents_minutes_ago, diff / 60_000)
+ diff < 86_400_000 -> stringResource(R.string.recents_hours_ago, diff / 3_600_000)
+ diff < 172_800_000 -> stringResource(R.string.recents_yesterday)
else -> SimpleDateFormat("MMM d", Locale.getDefault()).format(Date(ts))
}
}
+@Composable
private fun formatFileSize(bytes: Long): String = when {
- bytes < 1024 -> "$bytes B"
- bytes < 1024 * 1024 -> "${bytes / 1024} KB"
- else -> "${"%.1f".format(bytes / (1024.0 * 1024.0))} MB"
+ bytes < 1024 -> stringResource(R.string.recents_size_bytes, bytes)
+ bytes < 1024 * 1024 -> stringResource(R.string.recents_size_kb, bytes / 1024)
+ else -> stringResource(R.string.recents_size_mb, bytes / (1024.0 * 1024.0))
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ImagesToPdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ImagesToPdfScreen.kt
index 421c62c..dd1dc7f 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ImagesToPdfScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ImagesToPdfScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -116,9 +119,9 @@ fun ImagesToPdfScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar("Images → PDF", backdrop, uiSensor, Modifier.weight(1f), titleFontSize = 18.sp)
+ LiquidGlassTopBar(stringResource(R.string.images_to_pdf_title), backdrop, uiSensor, Modifier.weight(1f), titleFontSize = 18.sp)
}
Column(
@@ -134,11 +137,11 @@ fun ImagesToPdfScreen(
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
LiquidButton(onClick = { picker.launch("image/*") }, backdrop = backdrop, tint = accent) {
Icon(Icons.Rounded.AddPhotoAlternate, null, Modifier.size(18.dp), Color.White)
- BasicText("Add images", style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.images_add), style = TextStyle(Color.White, 14.sp, FontWeight.Medium))
}
if (state.imageUris.isNotEmpty()) {
LiquidButton(onClick = { viewModel.clearImages() }, backdrop = backdrop) {
- BasicText("Clear", style = TextStyle(text, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_clear), style = TextStyle(text, 14.sp, FontWeight.Medium))
}
}
}
@@ -148,17 +151,17 @@ fun ImagesToPdfScreen(
LiquidButton(
onClick = { viewModel.setFitToA4(true) }, backdrop = backdrop,
surfaceColor = if (state.fitToA4) accent.copy(0.30f) else Color.White.copy(0.06f)
- ) { BasicText("Fit to A4", style = TextStyle(text, 13.sp, FontWeight.Medium)) }
+ ) { BasicText(stringResource(R.string.images_fit_a4), style = TextStyle(text, 13.sp, FontWeight.Medium)) }
LiquidButton(
onClick = { viewModel.setFitToA4(false) }, backdrop = backdrop,
surfaceColor = if (!state.fitToA4) accent.copy(0.30f) else Color.White.copy(0.06f)
- ) { BasicText("Original size", style = TextStyle(text, 13.sp, FontWeight.Medium)) }
+ ) { BasicText(stringResource(R.string.images_original_size), style = TextStyle(text, 13.sp, FontWeight.Medium)) }
}
Box(Modifier.fillMaxWidth().weight(1f)) {
if (state.imageUris.isEmpty()) {
Box(Modifier.fillMaxSize(), Alignment.Center) {
- BasicText("Add photos to build a PDF.", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.images_hint), style = TextStyle(sub, 13.sp))
}
} else {
LazyVerticalGrid(
@@ -171,7 +174,7 @@ fun ImagesToPdfScreen(
Box(Modifier.aspectRatio(0.75f).clip(RoundedCornerShape(8.dp)).background(Color.White.copy(0.06f))) {
AsyncImage(
model = uri,
- contentDescription = "Image ${index + 1}",
+ contentDescription = stringResource(R.string.image_number, index + 1),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
@@ -181,7 +184,7 @@ fun ImagesToPdfScreen(
.clickable { viewModel.removeImage(index) },
Alignment.Center
) {
- Icon(Icons.Rounded.Close, "Remove", Modifier.size(14.dp), Color.White)
+ Icon(Icons.Rounded.Close, stringResource(R.string.delete), Modifier.size(14.dp), Color.White)
}
}
}
@@ -196,7 +199,7 @@ fun ImagesToPdfScreen(
BasicText(msg, style = TextStyle(Color(0xFFB9F6CA), 12.sp), modifier = Modifier.weight(1f))
state.lastOutputUri?.let { uri ->
LiquidButton(onClick = { onViewOutput(uri) }, backdrop = backdrop, tint = accent) {
- BasicText("Open", style = TextStyle(Color.White, 12.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.open), style = TextStyle(Color.White, 12.sp, FontWeight.Medium))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/MergePdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/MergePdfScreen.kt
index 16b2264..ee7ef7c 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/MergePdfScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/MergePdfScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -131,9 +134,9 @@ fun MergePdfScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar(title = "Merge PDFs", backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f))
+ LiquidGlassTopBar(title = stringResource(R.string.tool_merge), backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f))
}
Column(
@@ -155,8 +158,8 @@ fun MergePdfScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(Icons.AutoMirrored.Rounded.CallMerge, null, Modifier.size(56.dp), accent)
- BasicText("Combine Multiple PDFs", style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
- BasicText("Select two or more PDF files to merge into one", style = TextStyle(sub, 14.sp, textAlign = TextAlign.Center))
+ BasicText(stringResource(R.string.tool_merge), style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.tool_merge_description), style = TextStyle(sub, 14.sp, textAlign = TextAlign.Center))
LiquidButton(
onClick = { filePicker.launch(arrayOf("application/pdf")) },
@@ -164,7 +167,7 @@ fun MergePdfScreen(
) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Rounded.AddCircleOutline, null, Modifier.size(18.dp), Color.White)
- BasicText("Add Files", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.add_files), style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
}
}
}
@@ -177,8 +180,8 @@ fun MergePdfScreen(
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
- BasicText("${state.selectedFiles.size} files selected", style = TextStyle(text, 15.sp, fontWeight = FontWeight.Medium))
- BasicText("Reorder files before merging. Top to bottom = output order.", style = TextStyle(sub, 12.sp))
+ BasicText(stringResource(R.string.files_selected, state.selectedFiles.size), style = TextStyle(text, 15.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.reorder_files_hint), style = TextStyle(sub, 12.sp))
state.selectedFiles.forEachIndexed { index, file ->
Row(
Modifier.fillMaxWidth(),
@@ -215,7 +218,7 @@ fun MergePdfScreen(
if (state.selectedFiles.size < 2) {
Column(Modifier.fillMaxWidth().liquidGlassPanel(backdrop, uiSensor).padding(14.dp)) {
- BasicText("Select at least 2 PDFs to continue.", style = TextStyle(Color(0xFFD32F2F), 13.sp))
+ BasicText(stringResource(R.string.select_two_pdfs), style = TextStyle(Color(0xFFD32F2F), 13.sp))
}
}
@@ -257,7 +260,7 @@ fun MergePdfScreen(
tint = Color(0xFF1976D2),
modifier = Modifier.fillMaxWidth()
) {
- BasicText("View PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.viewer_open_pdf), style = TextStyle(Color.White, 15.sp, FontWeight.SemiBold))
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/OnboardingScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/OnboardingScreen.kt
index eaf133b..031b1b7 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/OnboardingScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/OnboardingScreen.kt
@@ -1,22 +1,23 @@
package com.chethan616.clearpdf.ui.screen
-import androidx.compose.ui.res.stringResource
-import com.chethan616.clearpdf.R
-import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -31,30 +32,25 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.AutoStories
import androidx.compose.material.icons.rounded.Check
-import androidx.compose.material.icons.rounded.Description
import androidx.compose.material.icons.rounded.Draw
-import androidx.compose.material.icons.rounded.Folder
-import androidx.compose.material.icons.rounded.Image
+import androidx.compose.material.icons.rounded.Language
+import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.PictureAsPdf
import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material.icons.rounded.TableChart
-import androidx.compose.material.icons.rounded.TextFields
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -64,13 +60,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.core.os.LocaleListCompat
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.data.repository.OnboardingManager
import com.chethan616.clearpdf.ui.components.LiquidButton
import com.chethan616.clearpdf.ui.components.liquidGlassPanel
@@ -78,193 +76,214 @@ import com.chethan616.clearpdf.ui.theme.LiquidGlassColors
import com.chethan616.clearpdf.ui.theme.LocalIsDarkMode
import com.chethan616.clearpdf.ui.utils.rememberUISensor
import com.kyant.backdrop.backdrops.LayerBackdrop
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-private data class Language(
- val name: String,
- val nativeName: String,
+private data class LanguageOption(
val code: String,
- val flag: String
+ val badge: String,
+ val labelRes: Int
)
private val SUPPORTED_LANGUAGES = listOf(
- Language("English", "English", "en", "🇬🇧"),
- Language("Portuguese (Brazil)", "Português (Brasil)", "pt-BR", "🇧🇷")
+ LanguageOption("en", "EN", R.string.language_english),
+ LanguageOption("pt-BR", "BR", R.string.language_portuguese)
)
-private data class FileTypeItem(
- val label: String,
- val resId: Int
+private data class FormatOption(
+ val labelRes: Int,
+ val drawableRes: Int
)
-private val FILE_TYPES = listOf(
- FileTypeItem("PDF", R.drawable.ic_format_pdf),
- FileTypeItem("Word", R.drawable.ic_format_word),
- FileTypeItem("Excel", R.drawable.ic_format_excel),
- FileTypeItem("PowerPoint", R.drawable.ic_format_ppt),
- FileTypeItem("Images", R.drawable.ic_format_image),
- FileTypeItem("TXT", R.drawable.ic_format_txt)
+private val FORMAT_OPTIONS = listOf(
+ FormatOption(R.string.file_type_pdf, R.drawable.ic_format_pdf),
+ FormatOption(R.string.file_type_word, R.drawable.ic_format_word),
+ FormatOption(R.string.file_type_excel, R.drawable.ic_format_excel),
+ FormatOption(R.string.file_type_powerpoint, R.drawable.ic_format_ppt),
+ FormatOption(R.string.file_type_images, R.drawable.ic_format_image),
+ FormatOption(R.string.file_type_txt, R.drawable.ic_format_txt)
)
-private data class FeatureItem(
+private data class FeatureOption(
val icon: ImageVector,
- val color: Color,
+ val accent: Color,
val titleRes: Int,
- val descRes: Int
+ val descriptionRes: Int
)
-private val FEATURES = listOf(
- FeatureItem(Icons.Rounded.Draw, Color(0xFF7E57C2), com.chethan616.clearpdf.R.string.feature_annotate_title, com.chethan616.clearpdf.R.string.feature_annotate_desc),
- FeatureItem(Icons.Rounded.Search, Color(0xFF0288D1), com.chethan616.clearpdf.R.string.feature_search_title, com.chethan616.clearpdf.R.string.feature_search_desc),
- FeatureItem(Icons.Rounded.Tune, Color(0xFF00897B), com.chethan616.clearpdf.R.string.feature_tools_title, com.chethan616.clearpdf.R.string.feature_tools_desc)
+private val FEATURE_OPTIONS = listOf(
+ FeatureOption(Icons.Rounded.Draw, Color(0xFF7D5CFF), R.string.feature_annotate_title, R.string.feature_annotate_desc),
+ FeatureOption(Icons.Rounded.Search, Color(0xFF4F9BFF), R.string.feature_search_title, R.string.feature_search_desc),
+ FeatureOption(Icons.Rounded.Tune, Color(0xFF33C88A), R.string.feature_tools_title, R.string.feature_tools_desc)
)
-/**
- * Apple HIG-inspired, 4-page onboarding experience.
- * Page 0: Language selector
- * Page 1: File compatibility showcase
- * Page 2: Feature highlights
- * Page 3: "Let's Go" CTA
- */
+/** A quiet, editorial onboarding flow for a private document workspace. */
@Composable
fun OnboardingScreen(
backdrop: LayerBackdrop,
- onComplete: () -> Unit
+ onComplete: () -> Unit,
+ selectedLocale: String = "en",
+ onLanguageChanged: (String) -> Unit = {}
) {
val context = LocalContext.current
- val isDarkMode = LocalIsDarkMode.current
+ val isDark = LocalIsDarkMode.current
val uiSensor = rememberUISensor()
- val scope = rememberCoroutineScope()
-
- var currentPage by remember { mutableIntStateOf(0) }
- var selectedLocale by remember {
- mutableStateOf(OnboardingManager.getSelectedLocale(context))
- }
-
- val totalPages = 4
-
- // Page entry animation
- var contentVisible by remember { mutableStateOf(false) }
- LaunchedEffect(currentPage) {
- contentVisible = false
- delay(80)
- contentVisible = true
- }
-
- val bgBrush = if (isDarkMode) {
- Brush.radialGradient(
- colors = listOf(Color(0xFF0D1117), Color(0xFF0A0A0F)),
- radius = 2000f
- )
+ var page by rememberSaveable { mutableIntStateOf(0) }
+ val pageCount = 4
+ val accents = listOf(
+ Color(0xFF4F7CFF),
+ Color(0xFFFF5F6D),
+ Color(0xFF7D5CFF),
+ Color(0xFF33C88A)
+ )
+ val accent = accents[page]
+ val text = if (isDark) Color(0xFFF4F7FF) else Color(0xFF182033)
+ val secondary = if (isDark) Color(0xFFB3BED2) else Color(0xFF62708A)
+ val background = if (isDark) {
+ Brush.linearGradient(listOf(Color(0xFF101728), Color(0xFF080B12)))
} else {
- Brush.radialGradient(
- colors = listOf(Color(0xFFF5F7FF), Color(0xFFECEFF5)),
- radius = 2000f
- )
- }
-
- val pageColor = when (currentPage) {
- 0 -> Color(0xFF6366F1) // Indigo
- 1 -> Color(0xFF00B0FF) // Bright Cyan
- 2 -> Color(0xFF8B5CF6) // Purple
- else -> Color(0xFF00C853) // Vivid Emerald
+ Brush.linearGradient(listOf(Color(0xFFF5F8FF), Color(0xFFE9EEFA)))
}
Box(
Modifier
.fillMaxSize()
- .background(bgBrush)
+ .background(background)
.statusBarsPadding()
.navigationBarsPadding()
) {
+ Box(
+ Modifier
+ .size(250.dp)
+ .align(Alignment.TopEnd)
+ .graphicsLayer { alpha = 0.22f }
+ .background(accent.copy(alpha = 0.28f), CircleShape)
+ )
+ Box(
+ Modifier
+ .size(180.dp)
+ .align(Alignment.BottomStart)
+ .graphicsLayer { alpha = 0.18f }
+ .background(LiquidGlassColors.Blue.copy(alpha = 0.28f), CircleShape)
+ )
+
Column(
Modifier
.fillMaxSize()
- .padding(horizontal = 24.dp, vertical = 20.dp),
+ .padding(horizontal = 22.dp, vertical = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- // Page dots indicator
Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.padding(bottom = 32.dp)
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- repeat(totalPages) { page ->
- val isActive = page == currentPage
- val dotWidth by animateDpAsState(
- if (isActive) 28.dp else 8.dp,
- spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessMedium),
- label = "dotWidth$page"
- )
- val dotAlpha by animateFloatAsState(
- if (isActive) 1f else 0.3f,
- tween(200), label = "dotAlpha$page"
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ Modifier
+ .size(38.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(accent.copy(alpha = 0.16f))
+ .border(1.dp, accent.copy(alpha = 0.30f), RoundedCornerShape(12.dp)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(Icons.Rounded.PictureAsPdf, null, Modifier.size(21.dp), accent)
+ }
+ Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
+ BasicText(
+ stringResource(R.string.app_name),
+ style = TextStyle(text, 16.sp, FontWeight.Bold)
+ )
+ BasicText(
+ stringResource(R.string.onboarding_welcome_subtitle),
+ style = TextStyle(secondary.copy(alpha = 0.72f), 10.sp, FontWeight.Medium)
+ )
+ }
+ }
+ BasicText(
+ stringResource(R.string.onboarding_step, page + 1, pageCount),
+ style = TextStyle(secondary, 12.sp, FontWeight.SemiBold)
+ )
+ }
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 18.dp, bottom = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ repeat(pageCount) { index ->
+ val selected = index == page
+ val width by animateFloatAsState(
+ if (selected) 1f else 0.32f,
+ tween(260, easing = FastOutSlowInEasing),
+ label = "onboardingProgress$index"
)
Box(
Modifier
- .width(dotWidth)
- .height(8.dp)
+ .weight(if (selected) width else 0.32f)
+ .height(4.dp)
.clip(CircleShape)
- .graphicsLayer { alpha = dotAlpha }
- .background(pageColor)
+ .background(if (selected) accent else secondary.copy(alpha = 0.18f))
)
}
}
- // Animated page content
AnimatedContent(
- targetState = currentPage,
+ targetState = page,
transitionSpec = {
- if (targetState > initialState) {
- (slideInHorizontally { it / 2 } + fadeIn(tween(280))).togetherWith(
- slideOutHorizontally { -it / 2 } + fadeOut(tween(180))
- )
+ val forward = targetState > initialState
+ if (forward) {
+ (slideInHorizontally { it / 3 } + fadeIn(tween(260)) + scaleIn(initialScale = 0.98f)) togetherWith
+ (slideOutHorizontally { -it / 4 } + fadeOut(tween(170)) + scaleOut(targetScale = 0.98f))
} else {
- (slideInHorizontally { -it / 2 } + fadeIn(tween(280))).togetherWith(
- slideOutHorizontally { it / 2 } + fadeOut(tween(180))
- )
+ (slideInHorizontally { -it / 3 } + fadeIn(tween(260)) + scaleIn(initialScale = 0.98f)) togetherWith
+ (slideOutHorizontally { it / 4 } + fadeOut(tween(170)) + scaleOut(targetScale = 0.98f))
}
},
- modifier = Modifier.weight(1f),
- label = "pageContent"
- ) { page ->
- when (page) {
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ label = "onboardingPage"
+ ) { currentPage ->
+ when (currentPage) {
0 -> LanguagePage(
backdrop = backdrop,
uiSensor = uiSensor,
+ isDark = isDark,
+ text = text,
+ secondary = secondary,
selectedLocale = selectedLocale,
- isDark = isDarkMode,
- onLanguageSelected = { code ->
- selectedLocale = code
- OnboardingManager.setSelectedLocale(context, code)
- com.chethan616.clearpdf.ui.utils.LocaleHelper.applyLocale(context, code, recreate = false)
- }
+ onLanguageSelected = onLanguageChanged
)
- 1 -> FilesPage(isDark = isDarkMode)
- 2 -> FeaturesPage(backdrop = backdrop, uiSensor = uiSensor, isDark = isDarkMode)
- 3 -> ReadyPage(isDark = isDarkMode)
+ 1 -> FormatsPage(backdrop, uiSensor, isDark, text, secondary)
+ 2 -> FeaturesPage(backdrop, uiSensor, isDark, text, secondary)
+ else -> ReadyPage(backdrop, uiSensor, isDark, text, secondary)
}
}
- Spacer(Modifier.height(24.dp))
+ Spacer(Modifier.height(14.dp))
- // Navigation button
+ // Keep the proven Backdrop button as the only primary action.
LiquidButton(
onClick = {
- if (currentPage < totalPages - 1) {
- currentPage++
- } else {
+ if (page < pageCount - 1) page++
+ else {
OnboardingManager.setOnboardingComplete(context)
onComplete()
}
},
backdrop = backdrop,
- tint = pageColor,
+ tint = accent,
modifier = Modifier.fillMaxWidth()
) {
BasicText(
- if (currentPage == totalPages - 1) stringResource(R.string.onboarding_cta_get_started) else stringResource(R.string.onboarding_cta_continue),
- style = TextStyle(Color.White, 17.sp, FontWeight.SemiBold),
+ stringResource(
+ if (page == pageCount - 1) R.string.onboarding_cta_get_started
+ else R.string.onboarding_cta_continue
+ ),
+ style = TextStyle(Color.White, 16.sp, FontWeight.SemiBold),
modifier = Modifier.padding(vertical = 10.dp)
)
}
@@ -272,86 +291,88 @@ fun OnboardingScreen(
}
}
-// ─── Page 0: Language Selector ────────────────────────────────────────────────
-
@Composable
private fun LanguagePage(
backdrop: LayerBackdrop,
uiSensor: com.chethan616.clearpdf.ui.utils.UISensor,
- selectedLocale: String,
isDark: Boolean,
+ text: Color,
+ secondary: Color,
+ selectedLocale: String,
onLanguageSelected: (String) -> Unit
) {
- val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF1A1A2E)
- val sub = if (isDark) Color(0xFFAAAAAA) else Color(0xFF666699)
-
Column(
- Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Top
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- // Hero icon
- Box(
- Modifier
- .size(100.dp)
- .clip(RoundedCornerShape(30.dp))
- .background(LiquidGlassColors.Blue.copy(alpha = 0.15f))
- .border(1.dp, LiquidGlassColors.Blue.copy(alpha = 0.3f), RoundedCornerShape(30.dp)),
- contentAlignment = Alignment.Center
- ) {
- BasicText("🌐", style = TextStyle(fontSize = 44.sp))
- }
-
- Spacer(Modifier.height(28.dp))
-
+ OnboardingArtworkCard(backdrop, uiSensor, R.drawable.ic_onboarding_language, Color(0xFF4F7CFF), isDark)
+ Spacer(Modifier.height(22.dp))
BasicText(
- stringResource(R.string.onboarding_language_title),
- style = TextStyle(text, 26.sp, FontWeight.Bold, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_welcome_title),
+ style = TextStyle(text, 29.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
Spacer(Modifier.height(8.dp))
BasicText(
- stringResource(R.string.onboarding_language_subtitle),
- style = TextStyle(sub, 15.sp, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_welcome_subtitle),
+ style = TextStyle(secondary, 15.sp, FontWeight.Medium, textAlign = TextAlign.Center)
)
-
- Spacer(Modifier.height(36.dp))
-
+ Spacer(Modifier.height(22.dp))
+ BasicText(
+ stringResource(R.string.onboarding_language_label),
+ style = TextStyle(secondary.copy(alpha = 0.86f), 12.sp, FontWeight.SemiBold)
+ )
+ Spacer(Modifier.height(9.dp))
Column(
- verticalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier
+ Modifier
.fillMaxWidth()
.liquidGlassPanel(backdrop, uiSensor)
- .padding(16.dp)
+ .padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- SUPPORTED_LANGUAGES.forEach { lang ->
- val isSelected = selectedLocale == lang.code
- val borderColor = if (isSelected) LiquidGlassColors.Blue else (if (isDark) Color.White.copy(0.12f) else Color.Black.copy(0.10f))
- val bgColor = if (isSelected) LiquidGlassColors.Blue.copy(0.18f) else (if (isDark) Color.White.copy(0.06f) else Color.White.copy(0.50f))
-
+ SUPPORTED_LANGUAGES.forEach { language ->
+ val selected = selectedLocale == language.code
+ val rowColor = if (selected) Color(0xFF4F7CFF).copy(alpha = 0.20f) else Color.White.copy(if (isDark) 0.06f else 0.48f)
Row(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
- .background(bgColor)
- .border(1.5.dp, borderColor, RoundedCornerShape(16.dp))
- .clickable(role = Role.RadioButton) { onLanguageSelected(lang.code) }
- .padding(16.dp),
+ .background(rowColor)
+ .border(
+ 1.dp,
+ if (selected) Color(0xFF4F7CFF) else secondary.copy(alpha = 0.16f),
+ RoundedCornerShape(16.dp)
+ )
+ .selectable(
+ selected = selected,
+ role = Role.RadioButton,
+ onClick = { onLanguageSelected(language.code) }
+ )
+ .padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(14.dp)
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
- BasicText(lang.flag, style = TextStyle(fontSize = 28.sp))
- Column(Modifier.weight(1f)) {
- BasicText(lang.nativeName, style = TextStyle(text, 16.sp, FontWeight.SemiBold))
- if (lang.nativeName != lang.name) {
- BasicText(lang.name, style = TextStyle(sub, 13.sp))
- }
+ Box(
+ Modifier
+ .size(38.dp)
+ .clip(RoundedCornerShape(11.dp))
+ .background(Color(0xFF4F7CFF).copy(alpha = if (selected) 0.28f else 0.12f)),
+ contentAlignment = Alignment.Center
+ ) {
+ BasicText(language.badge, style = TextStyle(Color(0xFF4F7CFF), 12.sp, FontWeight.Bold))
}
- if (isSelected) {
+ BasicText(
+ stringResource(language.labelRes),
+ style = TextStyle(text, 15.sp, if (selected) FontWeight.Bold else FontWeight.Medium),
+ modifier = Modifier.weight(1f)
+ )
+ if (selected) {
Box(
Modifier
- .size(24.dp)
+ .size(23.dp)
.clip(CircleShape)
- .background(LiquidGlassColors.Blue),
+ .background(Color(0xFF4F7CFF)),
contentAlignment = Alignment.Center
) {
Icon(Icons.Rounded.Check, null, Modifier.size(14.dp), Color.White)
@@ -363,55 +384,44 @@ private fun LanguagePage(
}
}
-// ─── Page 1: File Types ───────────────────────────────────────────────────────
-
@Composable
-private fun FilesPage(isDark: Boolean) {
- val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF1A1A2E)
- val sub = if (isDark) Color(0xFFAAAAAA) else Color(0xFF666699)
-
+private fun FormatsPage(
+ backdrop: LayerBackdrop,
+ uiSensor: com.chethan616.clearpdf.ui.utils.UISensor,
+ isDark: Boolean,
+ text: Color,
+ secondary: Color
+) {
Column(
- Modifier.fillMaxSize(),
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Box(
- Modifier
- .size(100.dp)
- .clip(RoundedCornerShape(30.dp))
- .background(Color(0xFF2E7D32).copy(0.15f))
- .border(1.dp, Color(0xFF2E7D32).copy(0.3f), RoundedCornerShape(30.dp)),
- contentAlignment = Alignment.Center
- ) {
- Icon(Icons.Rounded.Folder, null, Modifier.size(52.dp), Color(0xFF4CAF50))
- }
-
- Spacer(Modifier.height(28.dp))
-
+ OnboardingArtworkCard(backdrop, uiSensor, R.drawable.ic_onboarding_formats, Color(0xFFFF5F6D), isDark)
+ Spacer(Modifier.height(22.dp))
BasicText(
- stringResource(com.chethan616.clearpdf.R.string.onboarding_files_title),
- style = TextStyle(text, 26.sp, FontWeight.Bold, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_files_title),
+ style = TextStyle(text, 29.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
Spacer(Modifier.height(8.dp))
BasicText(
- stringResource(com.chethan616.clearpdf.R.string.onboarding_files_subtitle),
- style = TextStyle(sub, 15.sp, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_files_subtitle),
+ style = TextStyle(secondary, 15.sp, FontWeight.Medium, textAlign = TextAlign.Center)
)
-
- Spacer(Modifier.height(36.dp))
-
- // File type grid
- val rows = FILE_TYPES.chunked(3)
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- rows.forEach { row ->
- Row(
- Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- row.forEach { item ->
- FileTypePill(item, isDark, modifier = Modifier.weight(1f))
+ Spacer(Modifier.height(22.dp))
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .liquidGlassPanel(backdrop, uiSensor)
+ .padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(9.dp)
+ ) {
+ FORMAT_OPTIONS.chunked(3).forEach { row ->
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(9.dp)) {
+ row.forEach { option ->
+ FormatChip(option, isDark, text, Modifier.weight(1f))
}
- // Fill empty cells if last row has < 3 items
- repeat(3 - row.size) { Spacer(Modifier.weight(1f)) }
}
}
}
@@ -419,162 +429,202 @@ private fun FilesPage(isDark: Boolean) {
}
@Composable
-private fun FileTypePill(item: FileTypeItem, isDark: Boolean, modifier: Modifier) {
- val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF1A1A2E)
+private fun FormatChip(
+ option: FormatOption,
+ isDark: Boolean,
+ text: Color,
+ modifier: Modifier
+) {
Column(
modifier
- .clip(RoundedCornerShape(20.dp))
- .background(if (isDark) Color.White.copy(0.06f) else Color.Black.copy(0.04f))
- .border(1.dp, if (isDark) Color.White.copy(0.12f) else Color.Black.copy(0.08f), RoundedCornerShape(20.dp))
- .padding(vertical = 14.dp, horizontal = 4.dp),
+ .clip(RoundedCornerShape(15.dp))
+ .background(if (isDark) Color.White.copy(alpha = 0.07f) else Color.White.copy(alpha = 0.62f))
+ .border(1.dp, if (isDark) Color.White.copy(0.10f) else Color.Black.copy(0.07f), RoundedCornerShape(15.dp))
+ .padding(vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ verticalArrangement = Arrangement.spacedBy(6.dp)
) {
- androidx.compose.foundation.Image(
- painter = androidx.compose.ui.res.painterResource(item.resId),
- contentDescription = item.label,
- modifier = Modifier.size(42.dp)
+ Image(
+ painterResource(option.drawableRes),
+ contentDescription = stringResource(option.labelRes),
+ modifier = Modifier.size(35.dp)
+ )
+ BasicText(
+ stringResource(option.labelRes),
+ style = TextStyle(text, 11.sp, FontWeight.SemiBold, textAlign = TextAlign.Center)
)
- BasicText(item.label, style = TextStyle(text, 12.sp, FontWeight.SemiBold, textAlign = TextAlign.Center))
}
}
-// ─── Page 2: Features ─────────────────────────────────────────────────────────
-
@Composable
private fun FeaturesPage(
backdrop: LayerBackdrop,
uiSensor: com.chethan616.clearpdf.ui.utils.UISensor,
- isDark: Boolean
+ isDark: Boolean,
+ text: Color,
+ secondary: Color
) {
- val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF1A1A2E)
- val sub = if (isDark) Color(0xFFAAAAAA) else Color(0xFF666699)
-
Column(
- Modifier.fillMaxSize(),
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Box(
- Modifier
- .size(100.dp)
- .clip(RoundedCornerShape(30.dp))
- .background(Color(0xFF7E57C2).copy(0.15f))
- .border(1.dp, Color(0xFF7E57C2).copy(0.3f), RoundedCornerShape(30.dp)),
- contentAlignment = Alignment.Center
- ) {
- Icon(Icons.Rounded.Draw, null, Modifier.size(52.dp), Color(0xFF7E57C2))
- }
-
- Spacer(Modifier.height(28.dp))
-
+ OnboardingArtworkCard(backdrop, uiSensor, R.drawable.ic_onboarding_tools, Color(0xFF7D5CFF), isDark)
+ Spacer(Modifier.height(22.dp))
BasicText(
- stringResource(com.chethan616.clearpdf.R.string.onboarding_features_title),
- style = TextStyle(text, 26.sp, FontWeight.Bold, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_features_title),
+ style = TextStyle(text, 29.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
Spacer(Modifier.height(8.dp))
BasicText(
- stringResource(com.chethan616.clearpdf.R.string.onboarding_features_subtitle),
- style = TextStyle(sub, 15.sp, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_features_subtitle),
+ style = TextStyle(secondary, 15.sp, FontWeight.Medium, textAlign = TextAlign.Center)
)
-
- Spacer(Modifier.height(32.dp))
-
+ Spacer(Modifier.height(22.dp))
Column(
- verticalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier.fillMaxWidth()
+ Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(9.dp)
) {
- FEATURES.forEach { feature ->
+ FEATURE_OPTIONS.forEach { feature ->
Row(
Modifier
.fillMaxWidth()
- .clip(RoundedCornerShape(16.dp))
- .background(feature.color.copy(if (isDark) 0.1f else 0.07f))
- .border(1.dp, feature.color.copy(0.22f), RoundedCornerShape(16.dp))
- .padding(16.dp),
+ .clip(RoundedCornerShape(17.dp))
+ .background(feature.accent.copy(alpha = if (isDark) 0.12f else 0.08f))
+ .border(1.dp, feature.accent.copy(alpha = 0.22f), RoundedCornerShape(17.dp))
+ .padding(horizontal = 13.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(14.dp)
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
Modifier
- .size(44.dp)
+ .size(40.dp)
.clip(RoundedCornerShape(12.dp))
- .background(feature.color.copy(0.2f)),
+ .background(feature.accent.copy(alpha = 0.20f)),
contentAlignment = Alignment.Center
) {
- Icon(feature.icon, null, Modifier.size(24.dp), feature.color)
+ Icon(feature.icon, null, Modifier.size(21.dp), feature.accent)
}
- Column {
- BasicText(stringResource(feature.titleRes), style = TextStyle(text, 15.sp, FontWeight.SemiBold))
- BasicText(stringResource(feature.descRes), style = TextStyle(sub, 12.sp))
+ Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ BasicText(stringResource(feature.titleRes), style = TextStyle(text, 14.sp, FontWeight.Bold))
+ BasicText(stringResource(feature.descriptionRes), style = TextStyle(secondary, 11.sp, FontWeight.Medium))
}
+ Icon(Icons.Rounded.Check, null, Modifier.size(18.dp), feature.accent)
}
}
}
}
}
-// ─── Page 3: Ready CTA ────────────────────────────────────────────────────────
-
@Composable
-private fun ReadyPage(isDark: Boolean) {
- val text = if (isDark) Color(0xFFF0F0F0) else Color(0xFF1A1A2E)
- val sub = if (isDark) Color(0xFFAAAAAA) else Color(0xFF666699)
-
+private fun ReadyPage(
+ backdrop: LayerBackdrop,
+ uiSensor: com.chethan616.clearpdf.ui.utils.UISensor,
+ isDark: Boolean,
+ text: Color,
+ secondary: Color
+) {
Column(
- Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- // Animated check mark
- var visible by remember { mutableStateOf(false) }
- LaunchedEffect(Unit) { delay(300); visible = true }
- val iconScale by animateFloatAsState(
- if (visible) 1f else 0f,
- spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessMediumLow),
- label = "iconScale"
- )
-
- Box(
- Modifier
- .size(120.dp)
- .graphicsLayer { scaleX = iconScale; scaleY = iconScale }
- .clip(CircleShape)
- .background(Color(0xFF00C853).copy(0.15f))
- .border(2.dp, Color(0xFF00C853).copy(0.4f), CircleShape),
- contentAlignment = Alignment.Center
- ) {
- Icon(Icons.Rounded.Check, null, Modifier.size(60.dp), Color(0xFF00C853))
- }
-
- Spacer(Modifier.height(36.dp))
-
+ OnboardingArtworkCard(backdrop, uiSensor, R.drawable.ic_onboarding_ready, Color(0xFF33C88A), isDark)
+ Spacer(Modifier.height(26.dp))
BasicText(
- stringResource(com.chethan616.clearpdf.R.string.onboarding_ready_title),
- style = TextStyle(text, 28.sp, FontWeight.Bold, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_ready_title),
+ style = TextStyle(text, 30.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
-
- Spacer(Modifier.height(12.dp))
-
+ Spacer(Modifier.height(10.dp))
BasicText(
- stringResource(com.chethan616.clearpdf.R.string.onboarding_ready_subtitle),
- style = TextStyle(sub, 16.sp, textAlign = TextAlign.Center)
+ stringResource(R.string.onboarding_ready_subtitle),
+ style = TextStyle(secondary, 15.sp, FontWeight.Medium, textAlign = TextAlign.Center)
)
-
- Spacer(Modifier.height(48.dp))
-
- // Privacy badge
+ Spacer(Modifier.height(28.dp))
Row(
Modifier
- .clip(RoundedCornerShape(50))
- .background(LiquidGlassColors.Blue.copy(0.1f))
- .border(1.dp, LiquidGlassColors.Blue.copy(0.25f), RoundedCornerShape(50))
- .padding(horizontal = 16.dp, vertical = 8.dp),
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(18.dp))
+ .background(Color(0xFF33C88A).copy(alpha = if (isDark) 0.12f else 0.09f))
+ .border(1.dp, Color(0xFF33C88A).copy(alpha = 0.24f), RoundedCornerShape(18.dp))
+ .padding(horizontal = 15.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
- BasicText("🔒", style = TextStyle(fontSize = 14.sp))
- BasicText(stringResource(com.chethan616.clearpdf.R.string.onboarding_privacy_badge),
- style = TextStyle(sub, 12.sp, FontWeight.Medium))
+ Box(
+ Modifier
+ .size(38.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color(0xFF33C88A).copy(alpha = 0.20f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(Icons.Rounded.Lock, null, Modifier.size(20.dp), Color(0xFF33C88A))
+ }
+ BasicText(
+ stringResource(R.string.onboarding_privacy_badge),
+ style = TextStyle(text, 12.sp, FontWeight.SemiBold)
+ )
}
}
}
+
+@Composable
+private fun OnboardingArtworkCard(
+ backdrop: LayerBackdrop,
+ uiSensor: com.chethan616.clearpdf.ui.utils.UISensor,
+ drawableRes: Int,
+ accent: Color,
+ isDark: Boolean
+) {
+ val transition = rememberInfiniteTransition(label = "onboardingArtwork")
+ val drift by transition.animateFloat(
+ initialValue = -3f,
+ targetValue = 3f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(2400, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "artworkDrift"
+ )
+
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(208.dp)
+ .liquidGlassPanel(backdrop, uiSensor)
+ .clip(RoundedCornerShape(26.dp)),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ Modifier
+ .size(165.dp)
+ .align(Alignment.Center)
+ .clip(CircleShape)
+ .background(accent.copy(alpha = if (isDark) 0.16f else 0.10f))
+ )
+ Box(
+ Modifier
+ .size(116.dp)
+ .align(Alignment.Center)
+ .clip(CircleShape)
+ .background(Color.White.copy(alpha = if (isDark) 0.05f else 0.42f))
+ )
+ Image(
+ painterResource(drawableRes),
+ contentDescription = null,
+ modifier = Modifier
+ .size(230.dp)
+ .graphicsLayer { translationY = drift }
+ )
+ Box(
+ Modifier
+ .align(Alignment.TopEnd)
+ .padding(15.dp)
+ .size(8.dp)
+ .clip(CircleShape)
+ .background(accent)
+ )
+ }
+}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/PageOrganizerScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/PageOrganizerScreen.kt
index c320050..e6f3a67 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/PageOrganizerScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/PageOrganizerScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
@@ -158,9 +161,9 @@ fun PageOrganizerScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar("Organize Pages", backdrop, uiSensor, Modifier.weight(1f), titleFontSize = 18.sp)
+ LiquidGlassTopBar(stringResource(R.string.organize_screen_title), backdrop, uiSensor, Modifier.weight(1f), titleFontSize = 18.sp)
}
Column(
@@ -184,14 +187,14 @@ fun PageOrganizerScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- BasicText("Reorder, rotate & delete pages", style = TextStyle(text, 18.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.organize_title), style = TextStyle(text, 18.sp, fontWeight = FontWeight.SemiBold))
BasicText(
"Pick a PDF, drag pages to rearrange, then save a new copy. Original text & quality are preserved.",
style = TextStyle(sub, 13.sp)
)
LiquidButton(onClick = { picker.launch(arrayOf("application/pdf")) }, backdrop = backdrop, tint = accent) {
Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
- BasicText("Pick a PDF", style = TextStyle(Color.White, 15.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_pick_pdf), style = TextStyle(Color.White, 15.sp, FontWeight.Medium))
}
}
state.errorMessage?.let { BasicText(it, style = TextStyle(Color(0xFFD32F2F), 13.sp)) }
@@ -221,15 +224,15 @@ fun PageOrganizerScreen(
LiquidButton(onClick = { viewModel.rotateSelected() }, backdrop = backdrop) {
Icon(Icons.Rounded.RotateRight, null, Modifier.size(16.dp), text)
}
- LiquidButton(onClick = { viewModel.deleteSelected() }, backdrop = backdrop, tint = Color(0xFFEF5350)) {
+ LiquidButton(onClick = { viewModel.deleteSelected(context) }, backdrop = backdrop, tint = Color(0xFFEF5350)) {
Icon(Icons.Rounded.Delete, null, Modifier.size(16.dp), Color.White)
}
LiquidButton(onClick = { viewModel.clearSelection() }, backdrop = backdrop) {
- BasicText("Clear", style = TextStyle(text, 12.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_clear), style = TextStyle(text, 12.sp, FontWeight.Medium))
}
} else {
LiquidButton(onClick = { viewModel.selectAll() }, backdrop = backdrop) {
- BasicText("Select all", style = TextStyle(text, 12.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.select_all), style = TextStyle(text, 12.sp, FontWeight.Medium))
}
}
}
@@ -313,26 +316,26 @@ fun PageOrganizerScreen(
page.thumbnail?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
- contentDescription = "Page ${index + 1}",
+ contentDescription = stringResource(R.string.page_number, index + 1),
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize().rotate(page.rotation.toFloat())
)
}
}
Column(Modifier.weight(1f)) {
- BasicText("Page ${index + 1}", style = TextStyle(text, 14.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.create_page_label, index + 1), style = TextStyle(text, 14.sp, FontWeight.Medium))
BasicText(
"Source #${page.originalIndex + 1}" + if (page.rotation != 0) " · ${page.rotation}°" else "",
style = TextStyle(sub, 11.sp)
)
}
LiquidIconButton(onClick = { viewModel.rotatePage(index) }, backdrop = backdrop, surfaceColor = Color.White.copy(0.10f)) {
- Icon(Icons.Rounded.RotateRight, "Rotate", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.RotateRight, stringResource(R.string.organize_rotate), Modifier.size(18.dp), text)
}
LiquidIconButton(onClick = { viewModel.deletePage(index) }, backdrop = backdrop, surfaceColor = Color(0xFFD32F2F).copy(0.18f)) {
- Icon(Icons.Rounded.Delete, "Delete", Modifier.size(18.dp), Color(0xFFEF5350))
+ Icon(Icons.Rounded.Delete, stringResource(R.string.delete), Modifier.size(18.dp), Color(0xFFEF5350))
}
- Icon(Icons.Rounded.DragIndicator, "Drag", Modifier.size(22.dp), sub)
+ Icon(Icons.Rounded.DragIndicator, stringResource(R.string.organize_drag), Modifier.size(22.dp), sub)
}
}
}
@@ -390,7 +393,7 @@ fun PageOrganizerScreen(
BasicText(msg, style = TextStyle(Color(0xFFB9F6CA), 12.sp), modifier = Modifier.weight(1f))
state.lastOutputUri?.let { uri ->
LiquidButton(onClick = { onViewOutput(uri) }, backdrop = backdrop, tint = accent) {
- BasicText("Open", style = TextStyle(Color.White, 12.sp, FontWeight.Medium))
+ BasicText(stringResource(R.string.open), style = TextStyle(Color.White, 12.sp, FontWeight.Medium))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/PdfViewerScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/PdfViewerScreen.kt
index 936a1f7..99f89fd 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/PdfViewerScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/PdfViewerScreen.kt
@@ -8,11 +8,15 @@ import android.graphics.Bitmap
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.togetherWith
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
@@ -69,6 +73,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBackIosNew
@@ -118,11 +125,14 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
@@ -143,7 +153,6 @@ import com.chethan616.clearpdf.ui.utils.rememberUISensor
import com.chethan616.clearpdf.ui.viewmodel.OcrTextBlock
import com.chethan616.clearpdf.ui.viewmodel.PdfViewerViewModel
import com.kyant.backdrop.backdrops.LayerBackdrop
-import androidx.compose.foundation.text.BasicTextField
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.input.TextFieldValue
@@ -185,6 +194,7 @@ fun PdfViewerScreen(
val uiSensor = rememberUISensor()
val context = LocalContext.current
val clipboard = LocalClipboardManager.current
+ val focusManager = LocalFocusManager.current
val activity = context as? Activity
val view = LocalView.current
val scope = rememberCoroutineScope()
@@ -224,6 +234,8 @@ fun PdfViewerScreen(
var showFindBar by rememberSaveable { mutableStateOf(false) }
var findQuery by rememberSaveable { mutableStateOf("") }
val findFocusRequester = remember { FocusRequester() }
+ var passwordText by rememberSaveable { mutableStateOf("") }
+ val passwordFocusRequester = remember { FocusRequester() }
var showSignaturePad by remember { mutableStateOf(false) }
val haptic = LocalHapticFeedback.current
@@ -345,6 +357,14 @@ fun PdfViewerScreen(
uri?.let { viewModel.openPdf(context, it) }
}
+ LaunchedEffect(state.passwordUri) {
+ if (state.passwordUri != null) {
+ passwordText = ""
+ delay(120)
+ passwordFocusRequester.requestFocus()
+ }
+ }
+
if (state.document == null) {
Column(
Modifier.fillMaxSize().statusBarsPadding().padding(16.dp),
@@ -352,9 +372,14 @@ fun PdfViewerScreen(
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar("PDF Viewer", backdrop, uiSensor, Modifier.weight(1f))
+ LiquidGlassTopBar(
+ stringResource(R.string.viewer_title),
+ backdrop,
+ uiSensor,
+ Modifier.weight(1f)
+ )
}
Column(
Modifier.fillMaxWidth().weight(1f).verticalScroll(rememberScrollState()),
@@ -366,9 +391,12 @@ fun PdfViewerScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(Icons.Rounded.PictureAsPdf, null, Modifier.size(56.dp), accent)
- BasicText("Open a PDF", style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
BasicText(
- "Select a PDF file from your device to view it",
+ stringResource(R.string.viewer_open_pdf),
+ style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold)
+ )
+ BasicText(
+ stringResource(R.string.viewer_select_file),
style = TextStyle(sub, 14.sp, textAlign = TextAlign.Center)
)
LiquidButton(
@@ -380,12 +408,82 @@ fun PdfViewerScreen(
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
- BasicText("Pick a PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_pick_pdf), style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
}
}
}
state.errorMessage?.let { BasicText(it, style = TextStyle(Color(0xFFD32F2F), 14.sp)) }
}
+
+ AnimatedVisibility(
+ visible = state.passwordRequired && state.passwordUri != null,
+ enter = fadeIn() + scaleIn(initialScale = 0.96f),
+ exit = fadeOut() + scaleOut(targetScale = 0.98f),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ .imePadding()
+ ) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .liquidGlassPanel(backdrop, uiSensor)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ BasicText(
+ stringResource(R.string.pdf_password_title),
+ style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold)
+ )
+ BasicText(
+ if (state.passwordAttemptFailed) stringResource(R.string.pdf_password_wrong)
+ else stringResource(R.string.pdf_password_subtitle),
+ style = TextStyle(sub, 12.sp)
+ )
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicTextField(
+ value = passwordText,
+ onValueChange = { passwordText = it },
+ singleLine = true,
+ textStyle = TextStyle(text, 15.sp),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ state.passwordUri?.let { viewModel.openPdf(context, it, passwordText) }
+ }
+ ),
+ modifier = Modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(12.dp))
+ .background(if (isLight) Color.Black.copy(0.05f) else Color.White.copy(0.08f))
+ .padding(horizontal = 12.dp, vertical = 11.dp)
+ .focusRequester(passwordFocusRequester),
+ decorationBox = { inner ->
+ if (passwordText.isEmpty()) {
+ BasicText(stringResource(R.string.pdf_password_hint), style = TextStyle(sub, 14.sp))
+ }
+ inner()
+ }
+ )
+ LiquidButton(
+ onClick = {
+ state.passwordUri?.let { viewModel.openPdf(context, it, passwordText) }
+ },
+ backdrop = backdrop,
+ tint = accent
+ ) {
+ BasicText(stringResource(R.string.pdf_password_unlock), style = TextStyle(Color.White, 13.sp, FontWeight.SemiBold))
+ }
+ }
+ }
+ }
}
return
}
@@ -679,7 +777,7 @@ fun PdfViewerScreen(
) {
Image(
bitmap = bitmap.asImageBitmap(),
- contentDescription = "Page ${page + 1}",
+ contentDescription = stringResource(R.string.page_number, page + 1),
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
@@ -1160,7 +1258,7 @@ fun PdfViewerScreen(
modifier = Modifier.fillMaxWidth()
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.Black.copy(0.35f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), Color.White)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), Color.White)
}
LiquidGlassTopBar(
title = stringResource(R.string.viewer_page_of, state.currentPage + 1, safePageCount),
@@ -1196,21 +1294,51 @@ fun PdfViewerScreen(
val selectedTextCount = state.selectedOcrBlockIdsByPage[state.currentPage]?.size ?: 0
val currentSelectedIds = state.selectedOcrBlockIdsByPage[state.currentPage].orEmpty()
val hasEdits = annotationsByPage.values.any { it.isNotEmpty() }
+ val toolbarMode = when {
+ showFindBar -> ViewerToolbarMode.Search
+ showSignaturePad -> ViewerToolbarMode.Signature
+ activeImageId != null -> ViewerToolbarMode.Image
+ drawingToolActive -> ViewerToolbarMode.Drawing
+ activeTool == PdfEditTool.SelectText || selectedTextCount > 0 -> ViewerToolbarMode.Selection
+ activeTool == PdfEditTool.Eraser -> ViewerToolbarMode.Eraser
+ else -> ViewerToolbarMode.Main
+ }
AnimatedVisibility(
- visible = !showFindBar && activeImageId == null,
- enter = fadeIn(tween(200)) + androidx.compose.animation.slideInVertically { it / 2 },
- exit = fadeOut(tween(150)) + androidx.compose.animation.slideOutVertically { it / 2 }
+ visible = !showFindBar && !showSignaturePad && activeImageId == null,
+ enter = fadeIn(tween(220)) + expandVertically(
+ animationSpec = tween(260),
+ expandFrom = Alignment.Bottom
+ ) + scaleIn(initialScale = 0.96f),
+ exit = fadeOut(tween(150)) + shrinkVertically(
+ animationSpec = tween(180),
+ shrinkTowards = Alignment.Bottom
+ ) + scaleOut(targetScale = 0.96f)
) {
- Row(
- Modifier
- .fillMaxWidth()
- .liquidGlassPanel(backdrop, uiSensor)
- .padding(horizontal = 12.dp, vertical = 10.dp)
- .horizontalScroll(rememberScrollState()),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
+ AnimatedContent(
+ targetState = toolbarMode,
+ transitionSpec = {
+ (fadeIn(tween(180)) + expandVertically(
+ animationSpec = tween(220),
+ expandFrom = Alignment.Bottom
+ ) + scaleIn(initialScale = 0.97f)).togetherWith(
+ fadeOut(tween(120)) + shrinkVertically(
+ animationSpec = tween(150),
+ shrinkTowards = Alignment.Bottom
+ ) + scaleOut(targetScale = 0.97f)
+ )
+ },
+ label = "viewerToolMorph"
+ ) { _ ->
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .liquidGlassPanel(backdrop, uiSensor)
+ .padding(horizontal = 12.dp, vertical = 10.dp)
+ .horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
LiquidButton(
onClick = { activeTool = if (drawingToolActive) PdfEditTool.None else PdfEditTool.Draw },
backdrop = backdrop,
@@ -1223,7 +1351,7 @@ fun PdfViewerScreen(
tint = if (activeTool == PdfEditTool.SelectText) Color(0xFFAB47BC) else Color.Transparent
) {
BasicText(
- if (selectedTextCount > 0) "OCR ($selectedTextCount)" else stringResource(R.string.viewer_select_text),
+ if (selectedTextCount > 0) stringResource(R.string.viewer_ocr, selectedTextCount) else stringResource(R.string.viewer_select_text),
style = TextStyle(Color.White, 12.sp, FontWeight.Medium)
)
}
@@ -1258,7 +1386,11 @@ fun PdfViewerScreen(
onClick = {
showFindBar = !showFindBar
if (showFindBar) viewModel.triggerOcrForAllPages(context)
- else { viewModel.clearSearch(); findQuery = "" }
+ else {
+ focusManager.clearFocus()
+ viewModel.clearSearch()
+ findQuery = ""
+ }
},
backdrop = backdrop,
tint = if (showFindBar) Color(0xFF0288D1) else Color.Transparent
@@ -1304,7 +1436,10 @@ fun PdfViewerScreen(
backdrop = backdrop
) {
BasicText(
- "Reset Zoom ${(zoomScale * 100 + 0.5f).toInt()}%",
+ stringResource(
+ R.string.viewer_reset_zoom,
+ (zoomScale * 100 + 0.5f).toInt()
+ ),
style = TextStyle(Color.White, 12.sp, FontWeight.Medium)
)
}
@@ -1317,6 +1452,7 @@ fun PdfViewerScreen(
tint = Color(0xFF1976D2)
) { BasicText(stringResource(R.string.viewer_save_edits), style = TextStyle(Color.White, 12.sp, FontWeight.Medium)) }
}
+ }
}
}
@@ -1324,7 +1460,18 @@ fun PdfViewerScreen(
val showOcrTools = activeTool == PdfEditTool.SelectText || selectedTextCount > 0
val showImageTools = activeTool == PdfEditTool.Image && activeImageId != null
- AnimatedVisibility(visible = (showDrawTools || showOcrTools || showImageTools) && !showFindBar) {
+ AnimatedVisibility(
+ visible = (showDrawTools || showOcrTools || showImageTools) &&
+ !showFindBar && !showSignaturePad,
+ enter = fadeIn(tween(220)) + expandVertically(
+ animationSpec = tween(260),
+ expandFrom = Alignment.Bottom
+ ) + scaleIn(initialScale = 0.97f),
+ exit = fadeOut(tween(150)) + shrinkVertically(
+ animationSpec = tween(180),
+ shrinkTowards = Alignment.Bottom
+ ) + scaleOut(targetScale = 0.97f)
+ ) {
Column(
Modifier.fillMaxWidth().liquidGlassPanel(backdrop, uiSensor).padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -1529,7 +1676,26 @@ fun PdfViewerScreen(
.size(26.dp)
.clip(CircleShape)
.background(Color(cl))
- .clickable { currentColorLong = cl }
+ .clickable {
+ currentColorLong = cl
+ if (showImageTools) {
+ val marks = getPageMarks(state.currentPage)
+ val idx = marks.indexOfLast {
+ it is PdfMarkup.ImageMarkup &&
+ it.id == activeImageId &&
+ it.isSignature
+ }
+ val signature = marks.getOrNull(idx) as? PdfMarkup.ImageMarkup
+ if (signature != null) {
+ marks[idx] = signature.copy(
+ bitmap = recolorSignatureBitmap(
+ signature.bitmap,
+ Color(cl).toArgb()
+ )
+ )
+ }
+ }
+ }
.border(if (sel) 2.dp else 0.dp,
if (sel) Color.White else Color.Transparent,
CircleShape)
@@ -1558,7 +1724,7 @@ fun PdfViewerScreen(
) {
when {
state.isExporting -> BasicText(
- "Saving edited PDF…",
+ stringResource(R.string.viewer_saving),
style = TextStyle(Color.White.copy(0.82f), 12.sp)
)
state.exportError != null -> {
@@ -1566,7 +1732,12 @@ fun PdfViewerScreen(
LiquidButton(
onClick = { viewModel.clearExportFeedback() },
backdrop = backdrop
- ) { BasicText("Dismiss", style = TextStyle(Color.White, 11.sp, FontWeight.Medium)) }
+ ) {
+ BasicText(
+ stringResource(R.string.dismiss),
+ style = TextStyle(Color.White, 11.sp, FontWeight.Medium)
+ )
+ }
}
state.exportMessage != null -> {
BasicText(state.exportMessage!!, style = TextStyle(Color(0xFFB9F6CA), 12.sp))
@@ -1575,7 +1746,12 @@ fun PdfViewerScreen(
onClick = { viewModel.openPdf(context, uri) },
backdrop = backdrop,
tint = Color(0xFF1976D2)
- ) { BasicText("Open", style = TextStyle(Color.White, 11.sp, FontWeight.Medium)) }
+ ) {
+ BasicText(
+ stringResource(R.string.open),
+ style = TextStyle(Color.White, 11.sp, FontWeight.Medium)
+ )
+ }
}
}
}
@@ -1605,8 +1781,14 @@ fun PdfViewerScreen(
AnimatedVisibility(
visible = showFindBar,
- enter = fadeIn(tween(200)) + androidx.compose.animation.slideInVertically { it },
- exit = fadeOut(tween(150)) + androidx.compose.animation.slideOutVertically { it },
+ enter = fadeIn(tween(220)) + expandVertically(
+ animationSpec = tween(280),
+ expandFrom = Alignment.Bottom
+ ) + scaleIn(initialScale = 0.96f),
+ exit = fadeOut(tween(150)) + shrinkVertically(
+ animationSpec = tween(180),
+ shrinkTowards = Alignment.Bottom
+ ) + scaleOut(targetScale = 0.96f),
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
@@ -1693,7 +1875,7 @@ fun PdfViewerScreen(
surfaceColor = Color.White.copy(0.1f),
modifier = Modifier.size(32.dp)
) {
- Icon(Icons.Rounded.KeyboardArrowUp, "Previous", Modifier.size(18.dp), Color.White)
+ Icon(Icons.Rounded.KeyboardArrowUp, stringResource(R.string.previous), Modifier.size(18.dp), Color.White)
}
LiquidIconButton(
onClick = { viewModel.nextMatch(); lastInteractionAtMs = System.currentTimeMillis() },
@@ -1701,12 +1883,13 @@ fun PdfViewerScreen(
surfaceColor = Color.White.copy(0.1f),
modifier = Modifier.size(32.dp)
) {
- Icon(Icons.Rounded.KeyboardArrowDown, "Next", Modifier.size(18.dp), Color.White)
+ Icon(Icons.Rounded.KeyboardArrowDown, stringResource(R.string.next), Modifier.size(18.dp), Color.White)
}
LiquidIconButton(
onClick = {
showFindBar = false
+ focusManager.clearFocus()
findQuery = ""
viewModel.clearSearch()
},
@@ -1809,6 +1992,9 @@ private enum class ScrollOrientation { Vertical, Horizontal }
private enum class PdfEditTool { None, Draw, Highlight, Rect, Ellipse, Line, Arrow, SelectText, Image, Eraser }
+/** The large bottom control surface follows the user's current intent. */
+private enum class ViewerToolbarMode { Main, Drawing, Selection, Image, Eraser, Search, Signature }
+
@Composable
private fun PageScrubber(
currentPage: Int,
@@ -1856,7 +2042,7 @@ private fun PageScrubber(
label = "thumbFraction"
)
- val scrubberHeightDp = 280.dp
+ val scrubberHeightDp = 182.dp
val trackWidthDp by androidx.compose.animation.core.animateDpAsState(
targetValue = if (isDragging) 12.dp else 6.dp,
@@ -1865,9 +2051,19 @@ private fun PageScrubber(
)
Box(
- modifier = modifier.padding(end = 8.dp),
+ modifier = modifier
+ .padding(end = 4.dp)
+ .width(48.dp)
+ .height(scrubberHeightDp + 36.dp)
+ .liquidGlassPanel(backdrop, uiSensor)
+ .padding(horizontal = 8.dp, vertical = 8.dp),
contentAlignment = Alignment.CenterEnd
) {
+ BasicText(
+ text = "${currentPage + 1}",
+ style = TextStyle(color = accent, fontSize = 9.sp, fontWeight = FontWeight.Bold),
+ modifier = Modifier.align(Alignment.TopCenter)
+ )
Box(
modifier = Modifier
.width(40.dp)
@@ -1920,6 +2116,24 @@ private fun PageScrubber(
},
contentAlignment = Alignment.Center
) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .height(scrubberHeightDp)
+ .padding(vertical = 8.dp),
+ verticalArrangement = Arrangement.SpaceEvenly,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ repeat(pageCount.coerceAtMost(16)) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(2.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(if (isDarkMode) Color.White.copy(0.10f) else Color.Black.copy(0.08f))
+ )
+ }
+ }
Box(
modifier = Modifier
.width(trackWidthDp)
@@ -1951,15 +2165,15 @@ private fun PageScrubber(
scaleOut(targetScale = 0.9f)
) {
val previewBitmap = pageBitmaps.getOrNull(dragPage)
- val yOffsetDp = (scrubberHeightDp * animatedFraction - scrubberHeightDp / 2).coerceIn(-115.dp, 115.dp)
+ val yOffsetDp = (scrubberHeightDp * animatedFraction - scrubberHeightDp / 2).coerceIn(-78.dp, 78.dp)
Column(
modifier = Modifier
.align(Alignment.CenterEnd)
- .offset(x = (-46).dp, y = yOffsetDp)
- .width(145.dp)
+ .offset(x = (-42).dp, y = yOffsetDp)
+ .width(124.dp)
.liquidGlassPanel(backdrop, uiSensor)
- .padding(10.dp),
+ .padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -1967,24 +2181,24 @@ private fun PageScrubber(
Modifier
.clip(RoundedCornerShape(50))
.background(accent.copy(0.22f))
- .padding(horizontal = 10.dp, vertical = 4.dp),
+ .padding(horizontal = 8.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
BasicText(
- text = "PAGE",
- style = TextStyle(color = accent, fontSize = 9.sp, fontWeight = FontWeight.Bold)
+ text = stringResource(R.string.viewer_page_label),
+ style = TextStyle(color = accent, fontSize = 8.sp, fontWeight = FontWeight.Bold)
)
BasicText(
text = "${dragPage + 1} / $pageCount",
- style = TextStyle(color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.ExtraBold)
+ style = TextStyle(color = Color.White, fontSize = 11.sp, fontWeight = FontWeight.ExtraBold)
)
}
Box(
modifier = Modifier
.fillMaxWidth()
- .height(180.dp)
+ .height(140.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(0.45f))
.border(1.dp, Color.White.copy(0.15f), RoundedCornerShape(12.dp)),
@@ -1993,7 +2207,7 @@ private fun PageScrubber(
if (previewBitmap != null && !previewBitmap.isRecycled) {
Image(
bitmap = previewBitmap.asImageBitmap(),
- contentDescription = "Preview Page ${dragPage + 1}",
+ contentDescription = stringResource(R.string.preview_page, dragPage + 1),
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize().padding(4.dp)
)
@@ -2058,7 +2272,7 @@ private fun LiquidPageJumpDialog(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End)
) {
LiquidButton(onClick = onDismiss, backdrop = backdrop) {
- BasicText("Cancel", style = TextStyle(Color.White, 13.sp))
+ BasicText(stringResource(R.string.cancel), style = TextStyle(Color.White, 13.sp))
}
LiquidButton(
onClick = {
@@ -2068,13 +2282,28 @@ private fun LiquidPageJumpDialog(
backdrop = backdrop,
tint = Color(0xFF1976D2)
) {
- BasicText("Go", style = TextStyle(Color.White, 13.sp, fontWeight = FontWeight.Bold))
+ BasicText(stringResource(R.string.viewer_go), style = TextStyle(Color.White, 13.sp, FontWeight.Bold))
}
}
}
}
}
+private fun recolorSignatureBitmap(source: Bitmap, colorArgb: Int): Bitmap {
+ if (source.isRecycled || source.width <= 0 || source.height <= 0) return source
+
+ val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
+ val pixels = IntArray(source.width * source.height)
+ source.getPixels(pixels, 0, source.width, 0, 0, source.width, source.height)
+ val rgb = colorArgb and 0x00FFFFFF
+ for (index in pixels.indices) {
+ val alpha = android.graphics.Color.alpha(pixels[index])
+ pixels[index] = if (alpha == 0) 0 else (alpha shl 24) or rgb
+ }
+ result.setPixels(pixels, 0, source.width, 0, 0, source.width, source.height)
+ return result
+}
+
private sealed class PdfMarkup {
data class StrokeMarkup(
val points: List,
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ScanDocumentScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ScanDocumentScreen.kt
index 9073ff1..780296e 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ScanDocumentScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ScanDocumentScreen.kt
@@ -70,6 +70,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -77,6 +78,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.rememberAsyncImagePainter
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.model.ScanFilter
import com.chethan616.clearpdf.ui.components.LiquidButton
@@ -191,7 +193,7 @@ fun ScanDocumentScreen(
)
}
.addOnFailureListener { e ->
- viewModel.setError("Scanner not available: ${e.message}")
+ viewModel.setError(context.getString(R.string.scanner_start_failed))
viewModel.cancelScanning()
}
}
@@ -204,7 +206,7 @@ fun ScanDocumentScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
LiquidGlassTopBar(
- title = "Scan Document",
+ title = stringResource(R.string.scan_title),
backdrop = backdrop,
uiSensor = uiSensor,
modifier = Modifier
@@ -283,11 +285,11 @@ fun ScanDocumentScreen(
)
Spacer(Modifier.height(8.dp))
BasicText(
- "Scan Document",
+ stringResource(R.string.scan_title),
style = TextStyle(text, 20.sp, FontWeight.Bold, textAlign = TextAlign.Center)
)
BasicText(
- "Auto-detect edges, crop & enhance\nyour documents like a pro scanner",
+ stringResource(R.string.scan_description),
style = TextStyle(sub, 14.sp, textAlign = TextAlign.Center)
)
Spacer(Modifier.height(8.dp))
@@ -305,7 +307,7 @@ fun ScanDocumentScreen(
modifier = Modifier.padding(vertical = 12.dp)
) {
Icon(Icons.Rounded.CameraAlt, null, Modifier.size(20.dp), Color.White)
- BasicText("Scan Document", style = TextStyle(Color.White, 16.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.scan_document), style = TextStyle(Color.White, 16.sp, FontWeight.SemiBold))
}
}
@@ -326,7 +328,7 @@ fun ScanDocumentScreen(
modifier = Modifier.padding(vertical = 12.dp)
) {
Icon(Icons.Rounded.PhotoLibrary, null, Modifier.size(20.dp), text)
- BasicText("Import from Gallery", style = TextStyle(text, 16.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.import_gallery), style = TextStyle(text, 16.sp, FontWeight.SemiBold))
}
}
}
@@ -339,14 +341,14 @@ fun ScanDocumentScreen(
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
- BasicText("Features", style = TextStyle(text, 16.sp, FontWeight.Bold))
+ BasicText(stringResource(R.string.scan_features), style = TextStyle(text, 16.sp, FontWeight.Bold))
val features = listOf(
- Icons.Rounded.CropFree to "Auto edge detection & cropping",
- Icons.Rounded.CameraAlt to "Perspective correction",
- Icons.Rounded.DocumentScanner to "Multi-page document scanning",
- Icons.Rounded.PhotoLibrary to "Import from gallery",
- Icons.Rounded.PictureAsPdf to "Export as PDF"
+ Icons.Rounded.CropFree to stringResource(R.string.scan_feature_edges),
+ Icons.Rounded.CameraAlt to stringResource(R.string.scan_feature_perspective),
+ Icons.Rounded.DocumentScanner to stringResource(R.string.scan_feature_multi_page),
+ Icons.Rounded.PhotoLibrary to stringResource(R.string.scan_feature_gallery),
+ Icons.Rounded.PictureAsPdf to stringResource(R.string.scan_feature_export_pdf)
)
features.forEach { (icon, feature) ->
@@ -379,13 +381,13 @@ fun ScanDocumentScreen(
verticalAlignment = Alignment.CenterVertically
) {
BasicText(
- "${state.scannedPages.size} page${if (state.scannedPages.size != 1) "s" else ""} scanned",
+ if (state.scannedPages.size == 1) stringResource(R.string.scan_page_scanned) else stringResource(R.string.scan_pages_scanned, state.scannedPages.size),
style = TextStyle(text, 16.sp, FontWeight.SemiBold)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// Scan more pages
Icon(
- Icons.Rounded.DocumentScanner, "Scan more",
+ Icons.Rounded.DocumentScanner, stringResource(R.string.scan_more),
Modifier
.size(28.dp)
.clip(CircleShape)
@@ -395,7 +397,7 @@ fun ScanDocumentScreen(
accent
)
Icon(
- Icons.Rounded.PhotoLibrary, "Add from gallery",
+ Icons.Rounded.PhotoLibrary, stringResource(R.string.add_from_gallery),
Modifier
.size(28.dp)
.clip(CircleShape)
@@ -408,7 +410,7 @@ fun ScanDocumentScreen(
accent
)
Icon(
- Icons.Rounded.Delete, "Clear all",
+ Icons.Rounded.Delete, stringResource(R.string.clear_all),
Modifier
.size(28.dp)
.clip(CircleShape)
@@ -441,7 +443,7 @@ fun ScanDocumentScreen(
) {
Image(
painter = rememberAsyncImagePainter(page.uri),
- contentDescription = "Page ${index + 1}",
+ contentDescription = stringResource(R.string.page_number, index + 1),
contentScale = ContentScale.Crop,
colorFilter = getComposeColorFilter(page.filter),
modifier = Modifier.fillMaxSize()
@@ -465,7 +467,7 @@ fun ScanDocumentScreen(
// Delete button
Icon(
- Icons.Rounded.Close, "Remove page",
+ Icons.Rounded.Close, stringResource(R.string.remove_page),
Modifier
.align(Alignment.TopEnd)
.padding(6.dp)
@@ -488,7 +490,7 @@ fun ScanDocumentScreen(
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- BasicText("Filter", style = TextStyle(text, 14.sp, FontWeight.SemiBold))
+ BasicText(stringResource(R.string.filter), style = TextStyle(text, 14.sp, FontWeight.SemiBold))
Row(
Modifier
.fillMaxWidth()
@@ -496,11 +498,11 @@ fun ScanDocumentScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
val filters = listOf(
- ScanFilter.ORIGINAL to "Original",
- ScanFilter.AUTO to "Auto",
- ScanFilter.GRAYSCALE to "Grayscale",
- ScanFilter.BLACK_WHITE to "B&W",
- ScanFilter.COLOR to "Vivid"
+ ScanFilter.ORIGINAL to stringResource(R.string.scan_original),
+ ScanFilter.AUTO to stringResource(R.string.scan_auto),
+ ScanFilter.GRAYSCALE to stringResource(R.string.scan_grayscale),
+ ScanFilter.BLACK_WHITE to stringResource(R.string.scan_black_white),
+ ScanFilter.COLOR to stringResource(R.string.scan_vivid)
)
filters.forEach { (filter, label) ->
val currentFilter = state.scannedPages.firstOrNull()?.filter ?: ScanFilter.ORIGINAL
@@ -542,9 +544,9 @@ fun ScanDocumentScreen(
if (GitHubStarPromptManager.recordPdfInteraction(context)) {
StarPromptEventBus.requestPrompt()
}
- Toast.makeText(context, "PDF saved successfully!", Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, context.getString(R.string.pdf_saved_successfully), Toast.LENGTH_SHORT).show()
} else {
- viewModel.setError("Failed to save PDF")
+ viewModel.setError(context.getString(R.string.pdf_save_failed))
}
}
},
@@ -561,7 +563,7 @@ fun ScanDocumentScreen(
) {
Icon(Icons.Rounded.PictureAsPdf, null, Modifier.size(22.dp), Color.White)
BasicText(
- if (state.isScanning) "Saving..." else "Save as PDF",
+ if (state.isScanning) stringResource(R.string.saving) else stringResource(R.string.save_as_pdf),
style = TextStyle(Color.White, 16.sp, FontWeight.Bold)
)
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/SettingsScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/SettingsScreen.kt
index 6fcc0a6..f2c62b3 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/SettingsScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/SettingsScreen.kt
@@ -39,6 +39,7 @@ import androidx.compose.material.icons.rounded.FolderSpecial
import androidx.compose.material.icons.rounded.HighQuality
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.LightMode
+import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material.icons.rounded.PhoneAndroid
import androidx.compose.material.icons.rounded.Star
@@ -62,6 +63,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.data.repository.AppSettingsManager
import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
@@ -83,7 +86,9 @@ fun SettingsScreen(
isDarkMode: Boolean = false,
onDarkModeChanged: (Boolean) -> Unit = {},
themeMode: Int = 0,
- onThemeModeChanged: (Int) -> Unit = {}
+ onThemeModeChanged: (Int) -> Unit = {},
+ selectedLocale: String = "en",
+ onLocaleChanged: (String) -> Unit = {}
) {
val isLight = !isDarkMode
val text = if (isLight) Color(0xFF222222) else Color(0xFFF0F0F0)
@@ -224,7 +229,7 @@ fun SettingsScreen(
translationY = topBarOffsetY * density
}
) {
- LiquidGlassTopBar(title = "Settings", backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.fillMaxWidth())
+ LiquidGlassTopBar(title = stringResource(R.string.settings_title), backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.fillMaxWidth())
}
// ── Theme Mode Selector ──
@@ -244,7 +249,7 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Rounded.Tune, null, Modifier.size(22.dp), label)
- BasicText("Appearance", style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_appearance), style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
}
// Liquid Glass Theme Mode Selector
@@ -254,9 +259,9 @@ fun SettingsScreen(
) {
data class ThemeOption(val idx: Int, val label: String, val icon: ImageVector, val activeColor: Color)
val options = listOf(
- ThemeOption(0, "Auto", Icons.Rounded.PhoneAndroid, Color(0xFF0088FF)),
- ThemeOption(1, "Light", Icons.Rounded.LightMode, Color(0xFFFFA726)),
- ThemeOption(2, "Dark", Icons.Rounded.DarkMode, Color(0xFF7C4DFF))
+ ThemeOption(0, stringResource(R.string.settings_theme_auto), Icons.Rounded.PhoneAndroid, Color(0xFF0088FF)),
+ ThemeOption(1, stringResource(R.string.settings_theme_light), Icons.Rounded.LightMode, Color(0xFFFFA726)),
+ ThemeOption(2, stringResource(R.string.settings_theme_dark), Icons.Rounded.DarkMode, Color(0xFF7C4DFF))
)
options.forEach { option ->
val isSelected = themeMode == option.idx
@@ -292,9 +297,9 @@ fun SettingsScreen(
BasicText(
when (themeMode) {
- 1 -> "Always use light theme"
- 2 -> "Always use dark theme"
- else -> "Follows your device's system settings"
+ 1 -> stringResource(R.string.settings_theme_light_desc)
+ 2 -> stringResource(R.string.settings_theme_dark_desc)
+ else -> stringResource(R.string.settings_theme_auto_desc)
},
style = TextStyle(sub, 12.sp)
)
@@ -317,7 +322,7 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Rounded.FolderOpen, null, Modifier.size(22.dp), Color(0xFF0088FF))
- BasicText("Save Location", style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_save_location), style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
}
// Liquid Glass Save Location Mode Selector
@@ -349,7 +354,7 @@ fun SettingsScreen(
) {
Icon(Icons.Rounded.Download, null, Modifier.size(17.dp), defContentColor)
BasicText(
- "Downloads",
+ stringResource(R.string.settings_save_downloads),
style = TextStyle(
defContentColor,
13.sp,
@@ -375,7 +380,7 @@ fun SettingsScreen(
) {
Icon(Icons.Rounded.FolderSpecial, null, Modifier.size(17.dp), customContentColor)
BasicText(
- "Custom Folder",
+ stringResource(R.string.settings_save_custom),
style = TextStyle(
customContentColor,
13.sp,
@@ -414,12 +419,12 @@ fun SettingsScreen(
}
Column(Modifier.weight(1f)) {
BasicText(
- if (saveUri != null) "Custom Directory" else "Default Directory",
+ if (saveUri != null) stringResource(R.string.settings_custom_directory) else stringResource(R.string.settings_default_directory),
style = TextStyle(label, 13.sp, fontWeight = FontWeight.SemiBold)
)
val path = if (saveUri != null) {
saveUri!!.lastPathSegment?.replace("primary:", "") ?: saveUri.toString()
- } else "Downloads / ClearPDF"
+ } else stringResource(R.string.settings_default_path)
BasicText(path, style = TextStyle(sub, 12.sp))
}
}
@@ -443,13 +448,13 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Rounded.AutoFixHigh, null, Modifier.size(22.dp), label)
- BasicText("File Handling", style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_file_handling), style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
}
SettingsToggleRow(
icon = Icons.Rounded.AutoFixHigh,
- title = "Auto-Compress",
- desc = "Compress PDFs automatically on import",
+ title = stringResource(R.string.settings_auto_compress),
+ desc = stringResource(R.string.settings_auto_compress_desc),
checked = autoCompress,
onCheckedChange = { autoCompress = it; AppSettingsManager.setAutoCompress(context, it) },
backdrop = backdrop,
@@ -465,8 +470,8 @@ fun SettingsScreen(
SettingsToggleRow(
icon = Icons.Rounded.FileCopy,
- title = "Keep Original",
- desc = "Preserve original file after editing",
+ title = stringResource(R.string.settings_keep_original),
+ desc = stringResource(R.string.settings_keep_original_desc),
checked = keepOriginal,
onCheckedChange = { keepOriginal = it; AppSettingsManager.setKeepOriginal(context, it) },
backdrop = backdrop,
@@ -481,8 +486,8 @@ fun SettingsScreen(
SettingsToggleRow(
icon = Icons.Rounded.Notifications,
- title = "Notifications",
- desc = "Show notification when tasks complete",
+ title = stringResource(R.string.settings_notifications),
+ desc = stringResource(R.string.settings_notifications_desc),
checked = notifications,
onCheckedChange = { notifications = it; AppSettingsManager.setNotifications(context, it) },
backdrop = backdrop,
@@ -513,7 +518,7 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Rounded.HighQuality, null, Modifier.size(22.dp), Color(0xFF1976D2))
- BasicText("Compression Quality", style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_compression_quality), style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
}
Box(
Modifier
@@ -543,13 +548,12 @@ fun SettingsScreen(
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- BasicText("Smaller file size", style = TextStyle(sub.copy(0.7f), 11.sp))
- BasicText("Higher image clarity", style = TextStyle(sub.copy(0.7f), 11.sp))
+ BasicText(stringResource(R.string.settings_smaller_size), style = TextStyle(sub.copy(0.7f), 11.sp))
+ BasicText(stringResource(R.string.settings_higher_quality), style = TextStyle(sub.copy(0.7f), 11.sp))
}
}
// ── Language Selection ──
- val currentLang = remember { mutableStateOf(com.chethan616.clearpdf.data.repository.OnboardingManager.getSelectedLocale(context)) }
Column(
Modifier
.fillMaxWidth()
@@ -565,8 +569,8 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
- BasicText("🌐", style = TextStyle(fontSize = 20.sp))
- BasicText("Language / Idioma", style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
+ Icon(Icons.Rounded.Language, null, Modifier.size(22.dp), Color(0xFF0088FF))
+ BasicText(stringResource(R.string.settings_language), style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
}
Row(
@@ -574,18 +578,15 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
val languages = listOf(
- Pair("en", "🇬🇧 English"),
- Pair("pt-BR", "🇧🇷 Português")
+ Pair("en", stringResource(R.string.language_english)),
+ Pair("pt-BR", stringResource(R.string.language_portuguese))
)
languages.forEach { (code, labelText) ->
- val isSelected = currentLang.value == code
+ val isSelected = selectedLocale == code
val activeColor = Color(0xFF0088FF)
val contentColor = if (isSelected) Color.White else (if (isLight) Color(0xFF2C2C2E) else Color(0xFFE0E0E0))
LiquidButton(
- onClick = {
- currentLang.value = code
- com.chethan616.clearpdf.ui.utils.LocaleHelper.applyLocale(context, code, recreate = true)
- },
+ onClick = { onLocaleChanged(code) },
backdrop = backdrop,
tint = if (isSelected) activeColor else Color.Transparent,
surfaceColor = if (isSelected) activeColor.copy(0.18f) else (if (isLight) Color.White.copy(0.70f) else Color.White.copy(0.10f)),
@@ -626,9 +627,9 @@ fun SettingsScreen(
Icon(Icons.Rounded.Info, null, Modifier.size(28.dp), Color(0xFF0088FF))
}
BasicText("ClearPDF", style = TextStyle(text, 20.sp, fontWeight = FontWeight.Bold))
- BasicText("Version 1.0.0", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.settings_version), style = TextStyle(sub, 13.sp))
BasicText(
- "Made by Chethan616 with ❤",
+ stringResource(R.string.settings_made_by),
style = TextStyle(sub, 13.sp, textAlign = TextAlign.Center)
)
@@ -646,12 +647,12 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Star, null, Modifier.size(18.dp), Color.White)
- BasicText("Star on GitHub", style = TextStyle(Color.White, 14.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_star_github), style = TextStyle(Color.White, 14.sp, fontWeight = FontWeight.SemiBold))
}
}
BasicText(
- "ClearPDF is open source",
+ stringResource(R.string.settings_open_source),
style = TextStyle(sub.copy(0.7f), 11.sp, textAlign = TextAlign.Center)
)
}
@@ -673,7 +674,7 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Rounded.Code, null, Modifier.size(22.dp), label)
- BasicText("Open Source Licenses", style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.settings_licenses), style = TextStyle(text, 17.sp, fontWeight = FontWeight.SemiBold))
}
LicenseItem(
@@ -691,7 +692,7 @@ fun SettingsScreen(
)
BasicText(
- "Licensed under the Apache License, Version 2.0.\nYou may obtain a copy at apache.org/licenses/LICENSE-2.0",
+ stringResource(R.string.settings_license_notice),
style = TextStyle(sub.copy(0.7f), 11.sp, lineHeight = 16.sp)
)
}
@@ -775,7 +776,7 @@ private fun LicenseItem(
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
BasicText(name, style = TextStyle(labelColor, 14.sp, fontWeight = FontWeight.Medium))
- BasicText("by $author", style = TextStyle(subColor, 12.sp))
+ BasicText(stringResource(R.string.settings_license_author, author), style = TextStyle(subColor, 12.sp))
}
BasicText(license, style = TextStyle(subColor, 11.sp))
BasicText(url, style = TextStyle(Color(0xFF0088FF), 11.sp))
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/SignaturePadDialog.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/SignaturePadDialog.kt
index aa01b01..6b9c51a 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/SignaturePadDialog.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/SignaturePadDialog.kt
@@ -5,6 +5,10 @@ import android.graphics.Canvas
import android.graphics.Paint
import androidx.compose.ui.res.stringResource
import com.chethan616.clearpdf.R
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -18,6 +22,7 @@ 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.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -26,6 +31,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Close
@@ -33,10 +41,12 @@ import androidx.compose.material.icons.rounded.Undo
import androidx.compose.material.icons.rounded.Gesture
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -46,12 +56,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -62,6 +76,7 @@ import com.chethan616.clearpdf.ui.components.LiquidIconButton
import com.chethan616.clearpdf.ui.components.liquidGlassPanel
import com.chethan616.clearpdf.ui.utils.rememberUISensor
import com.kyant.backdrop.backdrops.LayerBackdrop
+import kotlinx.coroutines.delay
/**
* Full-screen signature capture dialog. Presents a dark canvas for the user to draw
@@ -98,28 +113,74 @@ fun SignaturePadDialog(
16f to stringResource(R.string.sig_heavy)
)
var selectedWidth by remember { androidx.compose.runtime.mutableFloatStateOf(6.5f) }
+ var contentVisible by remember { mutableStateOf(false) }
+ var showNamePrompt by remember { mutableStateOf(false) }
+ var signatureName by rememberSaveable { mutableStateOf("") }
+ var pendingSignature by remember { mutableStateOf(null) }
+ val nameFocusRequester = remember { FocusRequester() }
- val savedSignatures = remember {
- mutableStateListOf().apply {
+ LaunchedEffect(Unit) {
+ contentVisible = true
+ }
+
+ LaunchedEffect(showNamePrompt) {
+ if (showNamePrompt) {
+ delay(120)
+ runCatching { nameFocusRequester.requestFocus() }
+ }
+ }
+
+ data class SavedSignature(val name: String, val bitmap: Bitmap)
+ val savedSignatures = remember(context) {
+ mutableStateListOf().apply {
try {
- addAll(com.chethan616.clearpdf.data.repository.SignatureManager.listSignatures(context).mapNotNull { com.chethan616.clearpdf.data.repository.SignatureManager.loadSignature(it) })
+ com.chethan616.clearpdf.data.repository.SignatureManager
+ .listSignatures(context)
+ .mapNotNull { file ->
+ com.chethan616.clearpdf.data.repository.SignatureManager
+ .loadSignature(file)
+ ?.let { SavedSignature(
+ com.chethan616.clearpdf.data.repository.SignatureManager.displayName(file),
+ it
+ ) }
+ }
+ .forEach(::add)
} catch (_: Throwable) {}
}
}
+ val confirmSignatureName = {
+ val bitmap = pendingSignature
+ val trimmedName = signatureName.trim()
+ if (bitmap != null && trimmedName.isNotEmpty()) {
+ runCatching {
+ com.chethan616.clearpdf.data.repository.SignatureManager
+ .saveSignature(context, bitmap, trimmedName)
+ }
+ showNamePrompt = false
+ pendingSignature = null
+ onSignatureCaptured(bitmap)
+ }
+ }
+
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = true)
) {
- Column(
- Modifier
- .fillMaxSize()
- .background(Color(0xFF0A0A0A))
- .statusBarsPadding()
- .navigationBarsPadding()
- .padding(16.dp),
- verticalArrangement = Arrangement.SpaceBetween
+ AnimatedVisibility(
+ visible = contentVisible,
+ modifier = Modifier.fillMaxSize(),
+ enter = fadeIn(tween(220)) + scaleIn(initialScale = 0.98f)
) {
+ Column(
+ Modifier
+ .fillMaxSize()
+ .background(Color(0xFF0A0A0A))
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
// Header
Row(
Modifier.fillMaxWidth(),
@@ -132,7 +193,7 @@ fun SignaturePadDialog(
surfaceColor = Color.White.copy(0.08f),
modifier = Modifier.size(44.dp)
) {
- Icon(Icons.Rounded.Close, "Close", Modifier.size(20.dp), Color.White)
+ Icon(Icons.Rounded.Close, stringResource(R.string.close), Modifier.size(20.dp), Color.White)
}
BasicText(
@@ -146,7 +207,7 @@ fun SignaturePadDialog(
surfaceColor = Color.White.copy(0.08f),
modifier = Modifier.size(44.dp)
) {
- Icon(Icons.Rounded.Undo, "Undo", Modifier.size(20.dp), Color.White)
+ Icon(Icons.Rounded.Undo, stringResource(R.string.undo), Modifier.size(20.dp), Color.White)
}
}
@@ -273,7 +334,7 @@ fun SignaturePadDialog(
.background(Color.White.copy(0.15f))
)
BasicText(
- "Sign here",
+ stringResource(R.string.sig_hint),
style = TextStyle(Color.White.copy(0.2f), 14.sp),
modifier = Modifier
.align(Alignment.BottomCenter)
@@ -325,7 +386,7 @@ fun SignaturePadDialog(
if (savedSignatures.isNotEmpty()) {
Spacer(Modifier.height(10.dp))
BasicText(
- "Saved Signatures",
+ stringResource(R.string.sig_saved_title),
style = TextStyle(Color.White.copy(0.7f), 12.sp, FontWeight.SemiBold)
)
Row(
@@ -342,10 +403,10 @@ fun SignaturePadDialog(
.background(Color.White.copy(0.10f))
.border(1.dp, Color.White.copy(0.2f), RoundedCornerShape(10.dp))
.clickable {
- val safeBmp = if (savedBmp.config == Bitmap.Config.HARDWARE || !savedBmp.isMutable) {
- savedBmp.copy(Bitmap.Config.ARGB_8888, true)
+ val safeBmp = if (savedBmp.bitmap.config == Bitmap.Config.HARDWARE || !savedBmp.bitmap.isMutable) {
+ savedBmp.bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
- savedBmp
+ savedBmp.bitmap
}
onSignatureCaptured(safeBmp)
},
@@ -356,7 +417,7 @@ fun SignaturePadDialog(
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Gesture, null, Modifier.size(12.dp), Color.White)
- BasicText(stringResource(R.string.sig_saved), style = TextStyle(Color.White, 11.sp, FontWeight.Medium))
+ BasicText(savedBmp.name, style = TextStyle(Color.White, 11.sp, FontWeight.Medium))
}
}
}
@@ -366,7 +427,20 @@ fun SignaturePadDialog(
Spacer(Modifier.height(14.dp))
// Bottom actions
- Row(
+ if (showNamePrompt) {
+ SignatureNameBar(
+ backdrop = backdrop,
+ uiSensor = uiSensor,
+ name = signatureName,
+ focusRequester = nameFocusRequester,
+ onNameChange = { signatureName = it },
+ onCancel = {
+ showNamePrompt = false
+ pendingSignature = null
+ },
+ onSave = confirmSignatureName
+ )
+ } else Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
@@ -411,8 +485,9 @@ fun SignaturePadDialog(
stroke.drop(1).forEach { path.lineTo(it.x, it.y) }
canvas.drawPath(path, paint)
}
- runCatching { com.chethan616.clearpdf.data.repository.SignatureManager.saveSignature(context, bmp) }
- onSignatureCaptured(bmp)
+ pendingSignature = bmp
+ signatureName = ""
+ showNamePrompt = true
} catch (e: Throwable) {
e.printStackTrace()
}
@@ -437,3 +512,82 @@ fun SignaturePadDialog(
}
}
}
+}
+
+@Composable
+private fun SignatureNameBar(
+ backdrop: LayerBackdrop,
+ uiSensor: com.chethan616.clearpdf.ui.utils.UISensor,
+ name: String,
+ focusRequester: FocusRequester,
+ onNameChange: (String) -> Unit,
+ onCancel: () -> Unit,
+ onSave: () -> Unit
+) {
+ val canSave = name.trim().isNotEmpty()
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .imePadding()
+ .liquidGlassPanel(backdrop, uiSensor)
+ .padding(horizontal = 12.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Row(
+ Modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color.White.copy(0.12f))
+ .padding(horizontal = 10.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(Icons.Rounded.Gesture, null, Modifier.size(16.dp), Color.White.copy(0.6f))
+ Box(Modifier.weight(1f)) {
+ if (name.isEmpty()) {
+ BasicText(
+ stringResource(R.string.sig_name_hint),
+ style = TextStyle(Color.White.copy(0.45f), 13.sp)
+ )
+ }
+ BasicTextField(
+ value = name,
+ onValueChange = onNameChange,
+ textStyle = TextStyle(Color.White, 13.sp),
+ cursorBrush = SolidColor(Color.White),
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = { onSave() }),
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester)
+ )
+ }
+ }
+
+ LiquidButton(
+ onClick = onCancel,
+ backdrop = backdrop,
+ surfaceColor = Color.White.copy(0.08f),
+ modifier = Modifier.width(86.dp)
+ ) {
+ BasicText(
+ stringResource(R.string.cancel),
+ style = TextStyle(Color.White.copy(0.78f), 12.sp, FontWeight.Medium)
+ )
+ }
+ LiquidButton(
+ onClick = onSave,
+ backdrop = backdrop,
+ tint = if (canSave) Color(0xFF00C853) else Color.White.copy(0.08f),
+ modifier = Modifier.width(96.dp)
+ ) {
+ BasicText(
+ stringResource(R.string.sig_name_save),
+ style = TextStyle(Color.White.copy(if (canSave) 1f else 0.45f), 12.sp, FontWeight.SemiBold)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/SplitPdfScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/SplitPdfScreen.kt
index e4a3a86..77652bb 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/SplitPdfScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/SplitPdfScreen.kt
@@ -1,5 +1,8 @@
package com.chethan616.clearpdf.ui.screen
+import androidx.compose.ui.res.stringResource
+import com.chethan616.clearpdf.R
+
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -139,9 +142,9 @@ fun SplitPdfScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
LiquidButton(onClick = onBack, backdrop = backdrop, surfaceColor = Color.White.copy(0.08f)) {
- Icon(Icons.Rounded.ArrowBackIosNew, "Back", Modifier.size(18.dp), text)
+ Icon(Icons.Rounded.ArrowBackIosNew, stringResource(R.string.back), Modifier.size(18.dp), text)
}
- LiquidGlassTopBar(title = "Split PDF", backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f))
+ LiquidGlassTopBar(title = stringResource(R.string.tool_split), backdrop = backdrop, uiSensor = uiSensor, modifier = Modifier.weight(1f))
}
Column(
@@ -163,8 +166,8 @@ fun SplitPdfScreen(
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Icon(Icons.AutoMirrored.Rounded.CallSplit, null, Modifier.size(52.dp), accent)
- BasicText("Extract or split pages", style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
- BasicText("Pick one mode, then run one clear primary action.", style = TextStyle(sub, 13.sp, textAlign = TextAlign.Center))
+ BasicText(stringResource(R.string.tool_split), style = TextStyle(text, 20.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.tool_split_description), style = TextStyle(sub, 13.sp, textAlign = TextAlign.Center))
LiquidButton(
onClick = { filePicker.launch(arrayOf("application/pdf")) },
@@ -173,7 +176,7 @@ fun SplitPdfScreen(
) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Rounded.UploadFile, null, Modifier.size(18.dp), Color.White)
- BasicText("Select PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
+ BasicText(stringResource(R.string.viewer_pick_pdf), style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.Medium))
}
}
}
@@ -187,7 +190,7 @@ fun SplitPdfScreen(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
BasicText(state.sourceFileName, style = TextStyle(text, 15.sp, fontWeight = FontWeight.SemiBold))
- BasicText("${state.pageCount} pages", style = TextStyle(sub, 13.sp))
+ BasicText(stringResource(R.string.create_pages, state.pageCount), style = TextStyle(sub, 13.sp))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
@@ -232,10 +235,10 @@ fun SplitPdfScreen(
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- QuickSelectChip(label = "All", onClick = { viewModel.onSelectAllPages() }, text = text, accent = accent)
- QuickSelectChip(label = "Odd", onClick = { viewModel.onSelectOddPages() }, text = text, accent = accent)
- QuickSelectChip(label = "Even", onClick = { viewModel.onSelectEvenPages() }, text = text, accent = accent)
- QuickSelectChip(label = "Clear", onClick = { viewModel.onClearSelection() }, text = text, accent = accent)
+ QuickSelectChip(label = stringResource(R.string.split_quick_all), onClick = { viewModel.onSelectAllPages() }, text = text, accent = accent)
+ QuickSelectChip(label = stringResource(R.string.split_quick_odd), onClick = { viewModel.onSelectOddPages() }, text = text, accent = accent)
+ QuickSelectChip(label = stringResource(R.string.split_quick_even), onClick = { viewModel.onSelectEvenPages() }, text = text, accent = accent)
+ QuickSelectChip(label = stringResource(R.string.viewer_clear), onClick = { viewModel.onClearSelection() }, text = text, accent = accent)
}
FlowRow(
@@ -259,7 +262,7 @@ fun SplitPdfScreen(
if (thumb != null) {
Image(
bitmap = thumb.asImageBitmap(),
- contentDescription = "Page ${i + 1}",
+ contentDescription = stringResource(R.string.page_number, i + 1),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
@@ -357,7 +360,7 @@ fun SplitPdfScreen(
tint = Color(0xFF1976D2),
modifier = Modifier.fillMaxWidth()
) {
- BasicText("View PDF", style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.SemiBold))
+ BasicText(stringResource(R.string.viewer_open_pdf), style = TextStyle(Color.White, 15.sp, fontWeight = FontWeight.SemiBold))
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ToolsScreen.kt b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ToolsScreen.kt
index 418edcb..7c05bbd 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/screen/ToolsScreen.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/screen/ToolsScreen.kt
@@ -22,6 +22,8 @@ import androidx.compose.material.icons.automirrored.rounded.NoteAdd
import androidx.compose.material.icons.rounded.Compress
import androidx.compose.material.icons.rounded.FileOpen
import androidx.compose.material.icons.rounded.Image
+import androidx.compose.material.icons.rounded.Lock
+import androidx.compose.material.icons.rounded.LockOpen
import androidx.compose.material.icons.rounded.Reorder
import androidx.compose.material.icons.rounded.TextSnippet
import androidx.compose.material3.Icon
@@ -69,7 +71,9 @@ fun ToolsScreen(
onNavigateToCreatePdf: () -> Unit,
onNavigateToOrganizePdf: () -> Unit = {},
onNavigateToExtractText: () -> Unit = {},
- onNavigateToImagesToPdf: () -> Unit = {}
+ onNavigateToImagesToPdf: () -> Unit = {},
+ onNavigateToDecryptPdf: () -> Unit = {},
+ onNavigateToEncryptPdf: () -> Unit = {}
) {
val isDarkMode = LocalIsDarkMode.current
val uiSensor = rememberUISensor()
@@ -91,6 +95,10 @@ fun ToolsScreen(
val toolExtractSub = stringResource(R.string.tool_extract_sub)
val toolCreateTitle = stringResource(R.string.tool_create)
val toolCreateSub = stringResource(R.string.tool_create_sub)
+ val toolDecryptTitle = stringResource(R.string.tool_decrypt_pdf)
+ val toolDecryptSub = stringResource(R.string.tool_decrypt_pdf_sub)
+ val toolEncryptTitle = stringResource(R.string.tool_encrypt_pdf)
+ val toolEncryptSub = stringResource(R.string.tool_encrypt_pdf_sub)
val toolItems = remember(
toolOpenPdfTitle, toolOpenPdfSub,
@@ -101,6 +109,8 @@ fun ToolsScreen(
toolImagesTitle, toolImagesSub,
toolExtractTitle, toolExtractSub,
toolCreateTitle, toolCreateSub,
+ toolDecryptTitle, toolDecryptSub,
+ toolEncryptTitle, toolEncryptSub,
onNavigateToOpenPdf,
onNavigateToMergePdf,
onNavigateToSplitPdf,
@@ -108,7 +118,9 @@ fun ToolsScreen(
onNavigateToOrganizePdf,
onNavigateToImagesToPdf,
onNavigateToExtractText,
- onNavigateToCreatePdf
+ onNavigateToCreatePdf,
+ onNavigateToDecryptPdf,
+ onNavigateToEncryptPdf
) {
listOf(
ToolSpec("open", toolOpenPdfTitle, toolOpenPdfSub, LiquidGlassColors.Blue, Icons.Rounded.FileOpen, onNavigateToOpenPdf),
@@ -118,7 +130,9 @@ fun ToolsScreen(
ToolSpec("organize", toolOrganizeTitle, toolOrganizeSub, LiquidGlassColors.Teal, Icons.Rounded.Reorder, onNavigateToOrganizePdf),
ToolSpec("images", toolImagesTitle, toolImagesSub, LiquidGlassColors.Indigo, Icons.Rounded.Image, onNavigateToImagesToPdf),
ToolSpec("extract", toolExtractTitle, toolExtractSub, Color(0xFF5AC8FA), Icons.Rounded.TextSnippet, onNavigateToExtractText),
- ToolSpec("create", toolCreateTitle, toolCreateSub, LiquidGlassColors.Orange, Icons.AutoMirrored.Rounded.NoteAdd, onNavigateToCreatePdf)
+ ToolSpec("create", toolCreateTitle, toolCreateSub, LiquidGlassColors.Orange, Icons.AutoMirrored.Rounded.NoteAdd, onNavigateToCreatePdf),
+ ToolSpec("decrypt", toolDecryptTitle, toolDecryptSub, LiquidGlassColors.Purple, Icons.Rounded.LockOpen, onNavigateToDecryptPdf),
+ ToolSpec("encrypt", toolEncryptTitle, toolEncryptSub, LiquidGlassColors.Indigo, Icons.Rounded.Lock, onNavigateToEncryptPdf)
)
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/utils/LocaleHelper.kt b/app/src/main/java/com/chethan616/clearpdf/ui/utils/LocaleHelper.kt
index 6297df4..f53d9c3 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/utils/LocaleHelper.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/utils/LocaleHelper.kt
@@ -2,6 +2,7 @@ package com.chethan616.clearpdf.ui.utils
import android.content.Context
import android.content.res.Configuration
+import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.chethan616.clearpdf.data.repository.OnboardingManager
@@ -9,8 +10,15 @@ import java.util.Locale
object LocaleHelper {
+ private fun normalizeLanguageTag(languageTag: String): String {
+ val locale = Locale.forLanguageTag(languageTag.replace('_', '-'))
+ return if (locale.language.equals("pt", ignoreCase = true)) "pt-BR" else "en"
+ }
+
+ fun normalizeForUi(languageTag: String): String = normalizeLanguageTag(languageTag)
+
fun getLocalizedContext(context: Context, languageTag: String): Context {
- val locale = parseLocale(languageTag)
+ val locale = parseLocale(normalizeLanguageTag(languageTag))
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
@@ -18,39 +26,37 @@ object LocaleHelper {
}
private fun parseLocale(languageTag: String): Locale {
- return if (languageTag.contains("-")) {
- val parts = languageTag.split("-")
- Locale(parts[0], parts[1])
- } else if (languageTag.contains("_")) {
- val parts = languageTag.split("_")
- Locale(parts[0], parts[1])
- } else {
- Locale(languageTag)
- }
+ return Locale.forLanguageTag(languageTag.replace('_', '-'))
}
- fun applyLocale(context: Context, languageTag: String, recreate: Boolean = false) {
- OnboardingManager.setSelectedLocale(context, languageTag)
+ fun applyLocale(
+ context: Context,
+ languageTag: String,
+ recreate: Boolean = false,
+ updateAppCompat: Boolean = true
+ ) {
+ val normalizedTag = normalizeLanguageTag(languageTag)
+ OnboardingManager.setSelectedLocale(context, normalizedTag)
- val locale = parseLocale(languageTag)
+ val locale = parseLocale(normalizedTag)
Locale.setDefault(locale)
- val resources = context.resources
- val config = Configuration(resources.configuration)
- config.setLocale(locale)
- @Suppress("DEPRECATION")
- resources.updateConfiguration(config, resources.displayMetrics)
-
- val appRes = context.applicationContext.resources
- val appConfig = Configuration(appRes.configuration)
- appConfig.setLocale(locale)
- @Suppress("DEPRECATION")
- appRes.updateConfiguration(appConfig, appRes.displayMetrics)
+ // AppCompat owns the activity configuration on modern Android. The
+ // legacy resource update is kept only for API 23, where per-app
+ // language APIs are not available.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ val config = Configuration(context.resources.configuration)
+ config.setLocale(locale)
+ @Suppress("DEPRECATION")
+ context.resources.updateConfiguration(config, context.resources.displayMetrics)
+ }
- // Android 13+ / AppCompat per-app language
- AppCompatDelegate.setApplicationLocales(
- LocaleListCompat.forLanguageTags(languageTag)
- )
+ if (updateAppCompat) {
+ val requestedLocales = LocaleListCompat.forLanguageTags(normalizedTag)
+ if (AppCompatDelegate.getApplicationLocales() != requestedLocales) {
+ AppCompatDelegate.setApplicationLocales(requestedLocales)
+ }
+ }
if (recreate) {
(context as? android.app.Activity)?.recreate()
@@ -58,7 +64,7 @@ object LocaleHelper {
}
fun getLanguageDisplayName(languageTag: String): String {
- return when (languageTag) {
+ return when (normalizeLanguageTag(languageTag)) {
"pt-BR", "pt" -> "Português (Brasil)"
"en" -> "English"
else -> "English"
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CompressPdfViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CompressPdfViewModel.kt
index 3056d52..8d770d1 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CompressPdfViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CompressPdfViewModel.kt
@@ -14,6 +14,7 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.domain.usecase.CompressPdfUseCase
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import com.kyant.pdfcore.model.CompressionQuality
@@ -72,7 +73,7 @@ class CompressPdfViewModel(private val compressPdfUseCase: CompressPdfUseCase) :
estimatedSizeBytes = estimate
)
} catch (e: Exception) {
- _uiState.value = _uiState.value.copy(errorMessage = e.message)
+ _uiState.value = _uiState.value.copy(errorMessage = context.getString(R.string.open_pdf_failed))
}
}
}
@@ -162,7 +163,7 @@ class CompressPdfViewModel(private val compressPdfUseCase: CompressPdfUseCase) :
compressedSizeBytes = result.sizeBytes,
lastOutputUri = outUri,
saveLocationLabel = saveLabel,
- resultMessage = "Compressed: ${origKb}KB → ${compKb}KB (${reduction}% smaller)\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.compression_success, origKb, compKb, reduction, saveLabel)
)
if (GitHubStarPromptManager.recordPdfInteraction(context)) {
@@ -171,7 +172,7 @@ class CompressPdfViewModel(private val compressPdfUseCase: CompressPdfUseCase) :
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isCompressing = false,
- errorMessage = e.message ?: "Compression failed"
+ errorMessage = context.getString(R.string.compression_failed)
)
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CreatePdfViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CreatePdfViewModel.kt
index b13e459..b565960 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CreatePdfViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/CreatePdfViewModel.kt
@@ -16,6 +16,7 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.domain.usecase.CreatePdfUseCase
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import kotlinx.coroutines.Dispatchers
@@ -101,19 +102,19 @@ class CreatePdfViewModel(private val createPdfUseCase: CreatePdfUseCase) : ViewM
when (state.selectedMode) {
CreateMode.FROM_IMAGES -> {
if (state.selectedImageUris.isEmpty()) {
- _uiState.update { it.copy(errorMessage = "Please select at least one image") }
+ _uiState.update { it.copy(errorMessage = context.getString(R.string.create_min_image)) }
return
}
}
CreateMode.BLANK -> {
if (state.blankPageCount < 1) {
- _uiState.update { it.copy(errorMessage = "Blank PDF needs at least 1 page") }
+ _uiState.update { it.copy(errorMessage = context.getString(R.string.create_min_blank)) }
return
}
}
CreateMode.ADVANCED_TEXT -> {
if (state.textContent.isBlank()) {
- _uiState.update { it.copy(errorMessage = "Please enter some text") }
+ _uiState.update { it.copy(errorMessage = context.getString(R.string.create_text_required)) }
return
}
}
@@ -167,7 +168,7 @@ class CreatePdfViewModel(private val createPdfUseCase: CreatePdfUseCase) : ViewM
isCreating = false,
lastOutputUri = outputUri,
saveLocationLabel = saveLabel,
- resultMessage = "Created $fileName (${doc.pageCount} pages)\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.create_success, fileName, doc.pageCount, saveLabel)
)
}
@@ -176,7 +177,7 @@ class CreatePdfViewModel(private val createPdfUseCase: CreatePdfUseCase) : ViewM
}
} catch (e: Exception) {
_uiState.update {
- it.copy(isCreating = false, errorMessage = e.message ?: "Failed to create PDF")
+ it.copy(isCreating = false, errorMessage = context.getString(R.string.create_failed))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ExtractTextViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ExtractTextViewModel.kt
index ec0f321..450c5d8 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ExtractTextViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ExtractTextViewModel.kt
@@ -6,6 +6,7 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kyant.pdfcore.converter.PdfConverter
+import com.chethan616.clearpdf.R
import com.kyant.pdfcore.model.PdfDocument
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -42,11 +43,11 @@ class ExtractTextViewModel(private val converter: PdfConverter) : ViewModel() {
text = extracted,
hasResult = true,
errorMessage = if (extracted.isEmpty())
- "No selectable text found. This PDF may be a scan/image — try the scanner's OCR instead."
+ context.getString(R.string.extract_no_selectable_text)
else null
)
} catch (e: Exception) {
- _uiState.value = ExtractTextUiState(isExtracting = false, errorMessage = e.message ?: "Extraction failed")
+ _uiState.value = ExtractTextUiState(isExtracting = false, errorMessage = context.getString(R.string.text_extraction_failed))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ImagesToPdfViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ImagesToPdfViewModel.kt
index a8766d5..4a2c460 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ImagesToPdfViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/ImagesToPdfViewModel.kt
@@ -13,6 +13,7 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import com.kyant.pdfcore.converter.PdfConverter
import kotlinx.coroutines.Dispatchers
@@ -62,7 +63,7 @@ class ImagesToPdfViewModel(private val converter: PdfConverter) : ViewModel() {
fun save(context: Context, fileName: String, overrideUri: Uri? = null) {
val images = _uiState.value.imageUris
if (images.isEmpty()) {
- _uiState.value = _uiState.value.copy(errorMessage = "Add at least one image")
+ _uiState.value = _uiState.value.copy(errorMessage = context.getString(R.string.images_min_one))
return
}
_uiState.value = _uiState.value.copy(isSaving = true, errorMessage = null, resultMessage = null, lastOutputUri = null)
@@ -84,11 +85,11 @@ class ImagesToPdfViewModel(private val converter: PdfConverter) : ViewModel() {
isSaving = false,
lastOutputUri = outUri,
saveLocationLabel = saveLabel,
- resultMessage = "Created ${images.size}-page PDF\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.images_success, images.size, saveLabel)
)
if (GitHubStarPromptManager.recordPdfInteraction(context)) StarPromptEventBus.requestPrompt()
} catch (e: Exception) {
- _uiState.value = _uiState.value.copy(isSaving = false, errorMessage = e.message ?: "Conversion failed")
+ _uiState.value = _uiState.value.copy(isSaving = false, errorMessage = context.getString(R.string.conversion_failed))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/MergePdfViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/MergePdfViewModel.kt
index f89553e..a3ba911 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/MergePdfViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/MergePdfViewModel.kt
@@ -14,6 +14,7 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.domain.usecase.MergePdfUseCase
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import com.kyant.pdfcore.model.PdfDocument
@@ -94,7 +95,7 @@ class MergePdfViewModel(private val mergePdfUseCase: MergePdfUseCase) : ViewMode
fun onMerge(context: Context) {
val uris = _uiState.value.selectedUris
if (uris.size < 2) {
- _uiState.value = _uiState.value.copy(errorMessage = "Select at least 2 PDFs")
+ _uiState.value = _uiState.value.copy(errorMessage = context.getString(R.string.merge_min_files))
return
}
_uiState.value = _uiState.value.copy(isMerging = true, errorMessage = null, resultMessage = null, lastOutputUri = null)
@@ -118,7 +119,7 @@ class MergePdfViewModel(private val mergePdfUseCase: MergePdfUseCase) : ViewMode
isMerging = false,
lastOutputUri = outputUri,
saveLocationLabel = saveLabel,
- resultMessage = "Merged ${uris.size} files → $outName\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.merge_success, uris.size, outName, saveLabel)
)
if (GitHubStarPromptManager.recordPdfInteraction(context)) {
@@ -127,7 +128,7 @@ class MergePdfViewModel(private val mergePdfUseCase: MergePdfUseCase) : ViewMode
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isMerging = false,
- errorMessage = e.message ?: "Merge failed"
+ errorMessage = context.getString(R.string.merge_failed)
)
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PageOrganizerViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PageOrganizerViewModel.kt
index 85ff7d4..d18a5e6 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PageOrganizerViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PageOrganizerViewModel.kt
@@ -15,6 +15,7 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import com.kyant.pdfcore.editor.PdfEditor
import com.kyant.pdfcore.model.PdfDocument
@@ -70,7 +71,7 @@ class PageOrganizerViewModel(private val editor: PdfEditor) : ViewModel() {
pages = pages.mapIndexed { i, bmp -> OrganizerPage(originalIndex = i, thumbnail = bmp) }
)
} catch (e: Exception) {
- _uiState.value = PageOrganizerUiState(errorMessage = e.message ?: "Could not open PDF")
+ _uiState.value = PageOrganizerUiState(errorMessage = context.getString(R.string.organize_open_failed))
}
}
}
@@ -119,12 +120,12 @@ class PageOrganizerViewModel(private val editor: PdfEditor) : ViewModel() {
_uiState.value = _uiState.value.copy(pages = pages, resultMessage = null)
}
- fun deleteSelected() {
+ fun deleteSelected(context: Context) {
val sel = _uiState.value.selectedIds
if (sel.isEmpty()) return
val pages = _uiState.value.pages.filterNot { it.originalIndex in sel }
if (pages.isEmpty()) {
- _uiState.value = _uiState.value.copy(errorMessage = "Can't delete every page")
+ _uiState.value = _uiState.value.copy(errorMessage = context.getString(R.string.organize_cannot_delete_all))
return
}
_uiState.value = _uiState.value.copy(pages = pages, selectedIds = emptySet(), resultMessage = null)
@@ -176,12 +177,12 @@ class PageOrganizerViewModel(private val editor: PdfEditor) : ViewModel() {
isSaving = false,
lastOutputUri = outUri,
saveLocationLabel = saveLabel,
- resultMessage = "Saved ${pages.size} pages\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.organize_success, pages.size, saveLabel)
)
val shouldPrompt = withContext(Dispatchers.IO) { GitHubStarPromptManager.recordPdfInteraction(context) }
if (shouldPrompt) StarPromptEventBus.requestPrompt()
} catch (e: Exception) {
- _uiState.value = _uiState.value.copy(isSaving = false, errorMessage = e.message ?: "Save failed")
+ _uiState.value = _uiState.value.copy(isSaving = false, errorMessage = context.getString(R.string.organize_save_failed))
}
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PdfViewerViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PdfViewerViewModel.kt
index 0c977d1..408c46a 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PdfViewerViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/PdfViewerViewModel.kt
@@ -19,13 +19,16 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.domain.usecase.OpenPdfUseCase
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
import com.kyant.pdfcore.model.PdfDocument
+import com.kyant.pdfcore.security.PdfSecurityService
import com.chethan616.clearpdf.ui.utils.AppDispatchers
+import com.chethan616.clearpdf.utils.UniversalDocumentConverter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -50,6 +53,9 @@ data class PdfViewerUiState(
val currentPage: Int = 0,
val isLoading: Boolean = false,
val errorMessage: String? = null,
+ val passwordRequired: Boolean = false,
+ val passwordAttemptFailed: Boolean = false,
+ val passwordUri: Uri? = null,
val pageBitmaps: List = emptyList(),
val document: PdfDocument? = null,
val sizeBytes: Long = -1,
@@ -139,7 +145,7 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
private const val CACHE_RADIUS = 2
}
- fun openPdf(context: Context, uri: Uri) {
+ fun openPdf(context: Context, uri: Uri, password: String? = null) {
_uiState.value.document?.let { openPdfUseCase.close(it) }
recycleBitmaps(_uiState.value.pageBitmaps)
renderingPages.clear()
@@ -148,6 +154,11 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
_uiState.value = _uiState.value.copy(
isLoading = true,
errorMessage = null,
+ document = null,
+ pageBitmaps = emptyList(),
+ passwordRequired = false,
+ passwordAttemptFailed = false,
+ passwordUri = null,
ocrPagesInProgress = emptySet()
)
viewModelScope.launch {
@@ -161,8 +172,21 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
// Not all URI sources support persistable grants.
}
+ val sourceUri = withContext(Dispatchers.IO) {
+ when {
+ password != null -> {
+ val decrypted = PdfSecurityService.decryptToCache(context, uri, password)
+ FileProvider.getUriForFile(context, "${context.packageName}.provider", decrypted)
+ }
+ UniversalDocumentConverter.isPdf(context, uri) &&
+ PdfSecurityService.isPasswordProtected(context, uri) -> {
+ throw PdfSecurityService.PasswordRequiredException()
+ }
+ else -> uri
+ }
+ }
val (doc, openedUri) = withContext(Dispatchers.IO) {
- openDocumentWithFallback(context, uri)
+ openDocumentWithFallback(context, sourceUri)
}
val displayName = queryFileName(context, uri) ?: doc.name
_uiState.value = _uiState.value.copy(
@@ -170,6 +194,9 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
pageCount = doc.pageCount,
currentPage = 0,
isLoading = false,
+ passwordRequired = false,
+ passwordAttemptFailed = false,
+ passwordUri = null,
document = doc,
sizeBytes = doc.sizeBytes,
pageBitmaps = List(doc.pageCount) { null },
@@ -196,10 +223,18 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
// Render first page
renderPage(context, 0, DEFAULT_RENDER_WIDTH)
+ } catch (e: PdfSecurityService.PasswordRequiredException) {
+ _uiState.value = _uiState.value.copy(
+ isLoading = false,
+ passwordRequired = true,
+ passwordAttemptFailed = password != null,
+ passwordUri = uri,
+ errorMessage = null
+ )
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
- errorMessage = e.message ?: "Failed to open PDF"
+ errorMessage = context.getString(R.string.viewer_open_failed)
)
}
}
@@ -584,7 +619,7 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
_uiState.value = _uiState.value.copy(
isExporting = false,
- exportMessage = "Saved edited copy as $outputName",
+ exportMessage = context.getString(R.string.viewer_save_success, outputName),
lastExportedUri = outputUri
)
@@ -594,7 +629,7 @@ class PdfViewerViewModel(private val openPdfUseCase: OpenPdfUseCase) : ViewModel
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isExporting = false,
- exportError = e.message ?: "Failed to save edited PDF"
+ exportError = context.getString(R.string.viewer_save_failed)
)
}
}
diff --git a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/SplitPdfViewModel.kt b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/SplitPdfViewModel.kt
index 973cec9..e07f695 100644
--- a/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/SplitPdfViewModel.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/ui/viewmodel/SplitPdfViewModel.kt
@@ -15,6 +15,7 @@ import com.chethan616.clearpdf.data.repository.GitHubStarPromptManager
import com.chethan616.clearpdf.data.repository.RecentFile
import com.chethan616.clearpdf.data.repository.RecentFilesManager
import com.chethan616.clearpdf.data.repository.SaveLocationManager
+import com.chethan616.clearpdf.R
import com.chethan616.clearpdf.ui.utils.StarPromptEventBus
import com.kyant.pdfcore.model.PdfDocument
import com.kyant.pdfcore.splitter.PdfSplitter
@@ -101,7 +102,7 @@ class SplitPdfViewModel(private val splitter: PdfSplitter) : ViewModel() {
mode = SplitMode.EXTRACT_SELECTED
)
} catch (e: Exception) {
- _uiState.value = _uiState.value.copy(errorMessage = e.message)
+ _uiState.value = _uiState.value.copy(errorMessage = context.getString(R.string.open_pdf_failed))
}
}
}
@@ -172,7 +173,7 @@ class SplitPdfViewModel(private val splitter: PdfSplitter) : ViewModel() {
isSplitting = false,
lastOutputUri = firstOutputUri,
saveLocationLabel = saveLabel,
- resultMessage = "Split into ${results.size} pages\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.split_success, results.size, saveLabel)
)
if (GitHubStarPromptManager.recordPdfInteraction(context)) {
@@ -181,7 +182,7 @@ class SplitPdfViewModel(private val splitter: PdfSplitter) : ViewModel() {
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isSplitting = false,
- errorMessage = e.message ?: "Split failed"
+ errorMessage = context.getString(R.string.split_failed)
)
}
}
@@ -206,7 +207,7 @@ class SplitPdfViewModel(private val splitter: PdfSplitter) : ViewModel() {
isSplitting = false,
lastOutputUri = outUri,
saveLocationLabel = saveLabel,
- resultMessage = "Extracted ${pages.size} pages\nSaved to $saveLabel"
+ resultMessage = context.getString(R.string.extract_success, pages.size, saveLabel)
)
if (GitHubStarPromptManager.recordPdfInteraction(context)) {
@@ -215,7 +216,7 @@ class SplitPdfViewModel(private val splitter: PdfSplitter) : ViewModel() {
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isSplitting = false,
- errorMessage = e.message ?: "Extract failed"
+ errorMessage = context.getString(R.string.extract_failed)
)
}
}
@@ -226,7 +227,7 @@ class SplitPdfViewModel(private val splitter: PdfSplitter) : ViewModel() {
SplitMode.SPLIT_ALL -> onSplitAll(context)
SplitMode.EXTRACT_SELECTED -> {
if (_uiState.value.selectedPages.isEmpty()) {
- _uiState.value = _uiState.value.copy(errorMessage = "Select at least one page to extract")
+ _uiState.value = _uiState.value.copy(errorMessage = context.getString(R.string.extract_min_page))
return
}
onExtractPages(context, _uiState.value.selectedPages)
diff --git a/app/src/main/java/com/chethan616/clearpdf/utils/UniversalDocumentConverter.kt b/app/src/main/java/com/chethan616/clearpdf/utils/UniversalDocumentConverter.kt
index 01031a2..0e84dd5 100644
--- a/app/src/main/java/com/chethan616/clearpdf/utils/UniversalDocumentConverter.kt
+++ b/app/src/main/java/com/chethan616/clearpdf/utils/UniversalDocumentConverter.kt
@@ -15,6 +15,12 @@ import java.io.BufferedReader
import java.io.File
import java.io.FileOutputStream
import java.io.InputStreamReader
+import java.util.zip.ZipInputStream
+import org.xmlpull.v1.XmlPullParser
+import android.util.Xml
+import org.apache.poi.hssf.usermodel.HSSFWorkbook
+import org.apache.poi.hwpf.extractor.WordExtractor
+import org.apache.poi.hslf.usermodel.HSLFSlideShow
object UniversalDocumentConverter {
@@ -107,8 +113,20 @@ object UniversalDocumentConverter {
private fun convertGenericToPdf(context: Context, sourceUri: Uri): Uri {
val inputStream = context.contentResolver.openInputStream(sourceUri)
?: throw IllegalStateException("Unable to open file")
- val bytes = inputStream.readBytes()
- val contentStr = String(bytes, Charsets.UTF_8).filter { it.isISOControl().not() || it == '\n' || it == '\r' || it == '\t' }
+ val bytes = inputStream.use { it.readBytes() }
+ val fileName = getFileName(context, sourceUri).lowercase()
+ val structuredText = when {
+ fileName.endsWith(".docx") || fileName.endsWith(".pptx") || fileName.endsWith(".xlsx") || fileName.endsWith(".odt") ->
+ extractOfficeText(bytes.inputStream(), fileName)
+ fileName.endsWith(".doc") || fileName.endsWith(".xls") || fileName.endsWith(".ppt") ->
+ extractLegacyOfficeText(bytes.inputStream(), fileName)
+ else -> emptyList()
+ }
+ val contentStr = if (structuredText.isNotEmpty()) {
+ structuredText.joinToString("\n")
+ } else {
+ String(bytes, Charsets.UTF_8).filter { it.isISOControl().not() || it == '\n' || it == '\r' || it == '\t' }
+ }
val pdfDoc = PdfDocument()
val pageWidth = 595
@@ -146,6 +164,73 @@ object UniversalDocumentConverter {
return Uri.fromFile(outputFile)
}
+ /**
+ * Office Open XML files are ZIP containers. Reading their text nodes keeps the app
+ * dependency-light while still producing a useful, searchable PDF preview for Word,
+ * PowerPoint and Excel files.
+ */
+ private fun extractOfficeText(input: java.io.InputStream, fileName: String): List {
+ val lines = mutableListOf()
+ ZipInputStream(input).use { zip ->
+ while (true) {
+ val entry = zip.nextEntry ?: break
+ if (entry.isDirectory || !isRelevantOfficeEntry(entry.name, fileName)) continue
+ runCatching {
+ val parser = Xml.newPullParser().apply {
+ setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
+ setInput(zip, "UTF-8")
+ }
+ var event = parser.eventType
+ while (event != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.TEXT) {
+ parser.text.trim().takeIf { it.isNotEmpty() }?.let { lines += it }
+ }
+ event = parser.next()
+ }
+ }
+ zip.closeEntry()
+ if (lines.size >= 2000) break
+ }
+ }
+ return lines.take(2000)
+ }
+
+ private fun isRelevantOfficeEntry(entryName: String, fileName: String): Boolean {
+ return when {
+ fileName.endsWith(".docx") -> entryName.startsWith("word/") && entryName.endsWith(".xml") &&
+ !entryName.contains("styles") && !entryName.contains("settings") && !entryName.contains("rels")
+ fileName.endsWith(".pptx") -> entryName.startsWith("ppt/slides/") && entryName.endsWith(".xml")
+ fileName.endsWith(".xlsx") -> (entryName == "xl/sharedStrings.xml" || entryName.startsWith("xl/worksheets/")) && entryName.endsWith(".xml")
+ else -> entryName.endsWith(".xml")
+ }
+ }
+
+ private fun extractLegacyOfficeText(input: java.io.InputStream, fileName: String): List {
+ return try {
+ when {
+ fileName.endsWith(".doc") -> WordExtractor(input).use { it.paragraphText.toList() }
+ fileName.endsWith(".xls") -> HSSFWorkbook(input).use { workbook ->
+ workbook.iterator().asSequence().flatMap { sheet ->
+ sheet.iterator().asSequence().map { row ->
+ row.iterator().asSequence().joinToString("\t") { cell -> cell.toString() }
+ }
+ }.toList()
+ }
+ fileName.endsWith(".ppt") -> HSLFSlideShow(input).use { slideshow ->
+ slideshow.slides.flatMap { slide ->
+ slide.shapes.mapNotNull { shape ->
+ (shape as? org.apache.poi.sl.usermodel.TextShape<*, *>)?.text
+ }
+ }
+ }
+ else -> emptyList()
+ }.filter { it.isNotBlank() }.take(2000)
+ } catch (_: Throwable) {
+ // Some malformed legacy files are still better served by the readable-byte fallback.
+ emptyList()
+ }
+ }
+
private fun getFileName(context: Context, uri: Uri): String {
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
diff --git a/app/src/main/res/drawable/ic_onboarding_formats.xml b/app/src/main/res/drawable/ic_onboarding_formats.xml
new file mode 100644
index 0000000..356c6a9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_formats.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_language.xml b/app/src/main/res/drawable/ic_onboarding_language.xml
new file mode 100644
index 0000000..fab72d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_language.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_ready.xml b/app/src/main/res/drawable/ic_onboarding_ready.xml
new file mode 100644
index 0000000..8b6da06
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_ready.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_tools.xml b/app/src/main/res/drawable/ic_onboarding_tools.xml
new file mode 100644
index 0000000..e64426a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_tools.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 180fcf0..1539f0b 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -6,7 +6,7 @@
Abrir PDF
Abrir um arquivo PDF
Mesclar PDFs
- Mesclar múltiplos PDFs
+ Mesclar múltiplos PDFs
Comprimir
Comprimir um arquivo PDF
Criar PDF
@@ -16,40 +16,42 @@
Bem-vindo ao ClearPDF
Seu companheiro de documentos totalmente offline
Escolha seu idioma
- Personalize sua experiência
+ Personalize sua experiência
+ Idioma
Abra qualquer arquivo
- PDFs, documentos, planilhas, apresentações, imagens e muito mais
+ PDFs, documentos, planilhas, apresentações, imagens e muito mais
Ferramentas poderosas
Anote, assine, pesquise e organize seus documentos
- Tudo pronto! 🎉
- O ClearPDF está pronto para usar.\nTodos os seus documentos, sempre privados.
- 100% offline · Sem coleta de dados · Código aberto
+ Tudo pronto! 🎉
+ O ClearPDF está pronto para usar.\nTodos os seus documentos, sempre privados.
+ 100% offline · Sem coleta de dados · Código aberto
Continuar
- Começar
+ Começar
+ %1$d de %2$d
ClearPDF
- ESPAÇO DE TRABALHO PDF
+ ESPAÇO DE TRABALHO PDF
Trabalho com PDF, simplesmente elegante.
- Abra, digitalize e organize seus arquivos — privativamente no seu dispositivo.
+ Abra, digitalize e organize seus arquivos — privativamente no seu dispositivo.
Abrir PDF
Digitalizar
Recentes
Ver todos
Ver menos
Nenhum arquivo recente
- Abra um PDF para começar
+ Abra um PDF para começar
NO DISPOSITIVO
Ferramentas
- Tudo que você precisa para trabalhar com PDFs.
+ Tudo que você precisa para trabalhar com PDFs.
Abrir PDF
Visualizar e ler
Mesclar PDFs
Combinar arquivos
Dividir PDF
- Extrair páginas
+ Extrair páginas
Comprimir
Reduzir tamanho do arquivo
Organizar
@@ -62,12 +64,12 @@
Em branco, imagens, texto
- Configurações
- Aparência
+ Configurações
+ Aparência
Auto
Claro
Escuro
- Segue as configurações do sistema
+ Segue as configurações do sistema
Sempre usar tema claro
Sempre usar tema escuro
Local de Salvamento
@@ -77,44 +79,44 @@
Comprimir Automaticamente
Comprimir PDFs automaticamente ao importar
Manter Original
- Preservar arquivo original após edição
- Notificações
- Mostrar notificação quando tarefas concluírem
- Qualidade de Compressão
+ Preservar arquivo original após a edição
+ Notificações
+ Mostrar notificação quando tarefas concluírem
+ Qualidade de compressão
Menor tamanho
Maior qualidade
Idioma
Escolha o idioma do aplicativo
Sobre
- Versão 1.1.0
- Feito por Chethan616 com ❤
+ Versão 1.1.0
+ Feito por Chethan616 com ❤
Favoritar no GitHub
- ClearPDF é código aberto
- Licenças de Código Aberto
+ ClearPDF é código aberto
+ Licenças de código aberto
Visualizador de PDF
Abrir um PDF
Escolher um PDF
- Selecione um arquivo PDF do seu dispositivo para visualizá-lo
- Página %1$d / %2$d
+ Selecione um arquivo PDF do seu dispositivo para visualizá-lo
+ Página %1$d / %2$d
Ferramentas de Desenho
Selecionar Texto
Adicionar Imagem
Borracha
- Salvar Edições
+ Salvar Edições
Assinar
Buscar
- Buscar no documento…
+ Buscar no documento…
%1$d de %2$d resultados
Sem resultados
Anterior
- Próximo
+ Próximo
Abrir outro PDF
Caneta
Destaque
- Retângulo
- Óvalo
+ Retângulo
+ Óvalo
Linha
Seta
Desfazer
@@ -122,12 +124,13 @@
Selecionar Tudo
Sublinhado
Tachado
- Concluído
+ Concluído
Nova Assinatura
Substituir
- Salvando PDF editado…
+ Salvando PDF editado…
+ Redefinir zoom %1$d%%
Tamanho
- Ir para a página
+ Ir para a página
Desenhe sua assinatura
@@ -135,21 +138,25 @@
Limpar
Usar Assinatura
Fina
- Média
+ Média
Espessa
Forte
+ Assine aqui
+ Assinaturas salvas
+ Dê um nome para esta assinatura
+ Salvar nome
Anotar e Assinar
Desenhe, destaque e assine seus PDFs
Buscar e Selecionar
- Pesquise texto e selecione parágrafos inteiros
+ Pesquise texto e selecione parágrafos inteiros
Ferramentas Completas
Mescle, divida, comprima e converta
Voltar
- Concluído
+ Concluído
Cancelar
OK
Copiar
@@ -158,6 +165,197 @@
Abrir
Salvar
Dispensar
- Agora não
- Não perguntar novamente
+ Agora não
+ Não perguntar novamente
+ Diretório personalizado
+ Diretório padrão
+ Downloads / ClearPDF
+ Licenciado sob a Apache License, Versão 2.0.\nVocê pode obter uma cópia em apache.org/licenses/LICENSE-2.0
+ por %1$s
+ Inglês
+ Português
+ Abrir
+ Compartilhar
+ Detalhes
+ Remover
+ Compartilhar PDF
+ PDF
+ Pressione e segure para ações rápidas
+ Informações do arquivo
+ Páginas
+ Adicionado
+ Localização
+ Desconhecido
+ Agora
+ há %1$d min
+ há %1$d h
+ Ontem
+ %1$d KB
+ %1$.1f MB
+ %1$d B
+ %1$d páginas
+ Tamanho
+ Descriptografar PDF
+ Remover a senha de um PDF
+ Criptografar PDF
+ Proteger com uma senha
+ Digite a senha do PDF
+ Este documento está protegido. Digite a senha para continuar.
+ Senha
+ Desbloquear PDF
+ Essa senha não desbloqueou o PDF. Tente novamente.
+ Este PDF é protegido por senha.
+ Descriptografar PDF
+ Crie uma cópia desbloqueada sem alterar o original.
+ Selecionar PDF protegido
+ Senha do PDF
+ Salvar cópia desbloqueada
+ PDF desbloqueado salvo.
+ A senha está incorreta.
+ Ver PDF
+ Ver PDF criptografado
+ Ver PDF desbloqueado
+ Criptografar PDF
+ Crie uma cópia protegida por senha sem alterar o original.
+ Selecionar PDF para proteger
+ Criar senha do PDF
+ Confirmar senha
+ Salvar cópia criptografada
+ PDF criptografado salvo.
+ Digite uma senha para proteger o PDF.
+ As senhas não coincidem.
+ Este PDF já está protegido por senha.
+ Este tipo de arquivo ainda não pode ser visualizado.
+ Visualização de texto
+ Abrir documento
+ Página
+ Ir
+ OCR (%1$d)
+ Selecione dois ou mais PDFs para mesclar em um
+ Escolha um modo e execute uma ação principal clara.
+ %1$d arquivos selecionados
+ Reordene os arquivos antes de mesclar. De cima para baixo = ordem final.
+ Selecione pelo menos 2 PDFs para continuar.
+ Adicionar arquivos
+ Todos
+ Ímpares
+ Pares
+ Crie com digitalizações, imagens ou páginas em branco
+ Digitalize ou adicione imagens para iniciar seu rascunho.
+ Página %1$d
+ Toque nas setas para reordenar
+ Criar um PDF em branco
+ Defina o número de páginas e gere instantaneamente.
+ %1$d páginas
+ Texto avançado para PDF
+ Use para notas rápidas. Para documentos refinados, prefira digitalizar/imagem.
+ Digite o conteúdo...
+ %1$d caracteres
+ Importar imagens
+ Páginas em branco
+ Texto avançado
+ Criar PDF do rascunho
+ Criar PDF em branco
+ Criar PDF de texto
+ Adicionar imagens
+ Ajustar ao A4
+ Tamanho original
+ Adicione fotos para criar um PDF.
+ Compartilhar texto
+ Nenhuma camada de texto neste PDF.
+ Escolha um PDF para extrair o texto.
+ Reordenar, girar e excluir páginas
+ Selecionar tudo
+ Girar
+ Arrastar
+ Início
+ Ferramentas
+ Configurações
+ Fechar
+ Anterior
+ Próximo
+ Desfazer
+ Alterar pasta
+ Pasta selecionada
+ Salvar documento
+ Nome do arquivo
+ Documento.pdf
+ Digitalizar documento
+ Detecte bordas, recorte e melhore seus documentos como em um scanner profissional.
+ Digitalizar documento
+ Scanner indisponível: %1$s
+ Importar da galeria
+ Recursos
+ Detecção e recorte automáticos de bordas
+ Correção de perspectiva
+ Digitalização de documentos com várias páginas
+ Importar da galeria
+ Exportar como PDF
+ %1$d páginas digitalizadas
+ 1 página digitalizada
+ Digitalizar mais
+ Adicionar da galeria
+ Limpar tudo
+ Filtro
+ Original
+ Automático
+ Escala de cinza
+ P&B
+ Vívido
+ Salvando...
+ Salvar como PDF
+ PDF salvo com sucesso!
+ Falha ao salvar o PDF
+ Tamanho: %1$d KB
+ Prévia da página %1$d
+ Página %1$d
+ Remover página
+ Imagem %1$d
+ PDF
+ Word
+ Excel
+ PowerPoint
+ Imagens
+ TXT
+ Apoie o ClearPDF
+ O ClearPDF é um projeto de código aberto no GitHub.\nVocê gostaria de favoritar o projeto?
+ Sim, favoritar
+ Agora não
+ Não perguntar novamente
+ Algo deu errado. Tente novamente.
+ Não foi possível abrir este PDF.
+ Não foi possível salvar o PDF.
+ Selecione pelo menos 2 PDFs
+ %1$d arquivos mesclados -> %2$s\nSalvo em %3$s
+ Falha ao mesclar.
+ Dividido em %1$d páginas\nSalvo em %2$s
+ %1$d páginas extraídas\nSalvo em %2$s
+ Selecione pelo menos uma página para extrair
+ Falha ao dividir.
+ Falha ao extrair.
+ Adicione pelo menos uma imagem
+ PDF de %1$d páginas criado\nSalvo em %2$s
+ Falha na conversão.
+ Selecione pelo menos uma imagem
+ O PDF em branco precisa de pelo menos 1 página
+ Digite algum texto
+ Não foi possível criar o arquivo de saída.
+ Não foi possível ler as imagens.
+ %1$s criado (%2$d páginas)\nSalvo em %3$s
+ Falha ao criar o PDF.
+ Comprimido: %1$dKB -> %2$dKB (%3$d%% menor)\nSalvo em %4$s
+ Falha ao comprimir.
+ Nenhum texto selecionável encontrado. Este PDF pode ser uma digitalização ou imagem - tente o OCR do scanner.
+ Falha ao extrair o texto.
+ Não foi possível abrir o PDF.
+ Não é possível excluir todas as páginas.
+ %1$d páginas salvas\nSalvo em %2$s
+ Falha ao salvar.
+ Falha ao abrir o PDF.
+ Cópia editada salva como %1$s
+ Falha ao salvar o PDF editado.
+ Organizar páginas
+ Imagens -> PDF
+ O scanner está indisponível neste contexto.
+ Não foi possível iniciar o scanner.
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 180fcf0..87fcda8 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -6,7 +6,7 @@
Abrir PDF
Abrir um arquivo PDF
Mesclar PDFs
- Mesclar múltiplos PDFs
+ Mesclar múltiplos PDFs
Comprimir
Comprimir um arquivo PDF
Criar PDF
@@ -16,40 +16,42 @@
Bem-vindo ao ClearPDF
Seu companheiro de documentos totalmente offline
Escolha seu idioma
- Personalize sua experiência
+ Personalize sua experiência
+ Idioma
Abra qualquer arquivo
- PDFs, documentos, planilhas, apresentações, imagens e muito mais
+ PDFs, documentos, planilhas, apresentações, imagens e muito mais
Ferramentas poderosas
Anote, assine, pesquise e organize seus documentos
- Tudo pronto! 🎉
- O ClearPDF está pronto para usar.\nTodos os seus documentos, sempre privados.
- 100% offline · Sem coleta de dados · Código aberto
+ Tudo pronto! 🎉
+ O ClearPDF está pronto para usar.\nTodos os seus documentos, sempre privados.
+ 100% offline · Sem coleta de dados · Código aberto
Continuar
- Começar
+ Começar
+ %1$d de %2$d
ClearPDF
- ESPAÇO DE TRABALHO PDF
+ ESPAÇO DE TRABALHO PDF
Trabalho com PDF, simplesmente elegante.
- Abra, digitalize e organize seus arquivos — privativamente no seu dispositivo.
+ Abra, digitalize e organize seus arquivos — privativamente no seu dispositivo.
Abrir PDF
Digitalizar
Recentes
Ver todos
Ver menos
Nenhum arquivo recente
- Abra um PDF para começar
+ Abra um PDF para começar
NO DISPOSITIVO
Ferramentas
- Tudo que você precisa para trabalhar com PDFs.
+ Tudo que você precisa para trabalhar com PDFs.
Abrir PDF
Visualizar e ler
Mesclar PDFs
Combinar arquivos
Dividir PDF
- Extrair páginas
+ Extrair páginas
Comprimir
Reduzir tamanho do arquivo
Organizar
@@ -62,12 +64,12 @@
Em branco, imagens, texto
- Configurações
- Aparência
+ Configurações
+ Aparência
Auto
Claro
Escuro
- Segue as configurações do sistema
+ Segue as configurações do sistema
Sempre usar tema claro
Sempre usar tema escuro
Local de Salvamento
@@ -77,44 +79,44 @@
Comprimir Automaticamente
Comprimir PDFs automaticamente ao importar
Manter Original
- Preservar arquivo original após edição
- Notificações
- Mostrar notificação quando tarefas concluírem
- Qualidade de Compressão
+ Preservar arquivo original após a edição
+ Notificações
+ Mostrar notificação quando tarefas concluírem
+ Qualidade de compressão
Menor tamanho
Maior qualidade
Idioma
Escolha o idioma do aplicativo
Sobre
- Versão 1.1.0
- Feito por Chethan616 com ❤
+ Versão 1.1.0
+ Feito por Chethan616 com ❤
Favoritar no GitHub
- ClearPDF é código aberto
- Licenças de Código Aberto
+ ClearPDF é código aberto
+ Licenças de código aberto
Visualizador de PDF
Abrir um PDF
Escolher um PDF
- Selecione um arquivo PDF do seu dispositivo para visualizá-lo
- Página %1$d / %2$d
+ Selecione um arquivo PDF do seu dispositivo para visualizá-lo
+ Página %1$d / %2$d
Ferramentas de Desenho
Selecionar Texto
Adicionar Imagem
Borracha
- Salvar Edições
+ Salvar Edições
Assinar
Buscar
- Buscar no documento…
+ Buscar no documento…
%1$d de %2$d resultados
Sem resultados
Anterior
- Próximo
+ Próximo
Abrir outro PDF
Caneta
Destaque
- Retângulo
- Óvalo
+ Retângulo
+ Óvalo
Linha
Seta
Desfazer
@@ -122,12 +124,13 @@
Selecionar Tudo
Sublinhado
Tachado
- Concluído
+ Concluído
Nova Assinatura
Substituir
- Salvando PDF editado…
+ Salvando PDF editado…
+ Redefinir zoom %1$d%%
Tamanho
- Ir para a página
+ Ir para a página
Desenhe sua assinatura
@@ -135,21 +138,25 @@
Limpar
Usar Assinatura
Fina
- Média
+ Média
Espessa
Forte
+ Assine aqui
+ Assinaturas salvas
+ Dê um nome para esta assinatura
+ Salvar nome
Anotar e Assinar
Desenhe, destaque e assine seus PDFs
Buscar e Selecionar
- Pesquise texto e selecione parágrafos inteiros
+ Pesquise texto e selecione parágrafos inteiros
Ferramentas Completas
Mescle, divida, comprima e converta
Voltar
- Concluído
+ Concluído
Cancelar
OK
Copiar
@@ -158,6 +165,197 @@
Abrir
Salvar
Dispensar
- Agora não
- Não perguntar novamente
+ Agora não
+ Não perguntar novamente
+ Diretório personalizado
+ Diretório padrão
+ Downloads / ClearPDF
+ Licenciado sob a Apache License, Versão 2.0.\nVocê pode obter uma cópia em apache.org/licenses/LICENSE-2.0
+ por %1$s
+ Inglês
+ Português
+ Abrir
+ Compartilhar
+ Detalhes
+ Remover
+ Compartilhar PDF
+ PDF
+ Pressione e segure para ações rápidas
+ Informações do arquivo
+ Páginas
+ Adicionado
+ Localização
+ Desconhecido
+ Agora
+ há %1$d min
+ há %1$d h
+ Ontem
+ %1$d KB
+ %1$.1f MB
+ %1$d B
+ %1$d páginas
+ Tamanho
+ Descriptografar PDF
+ Remover a senha de um PDF
+ Criptografar PDF
+ Proteger com uma senha
+ Digite a senha do PDF
+ Este documento está protegido. Digite a senha para continuar.
+ Senha
+ Desbloquear PDF
+ Essa senha não desbloqueou o PDF. Tente novamente.
+ Este PDF é protegido por senha.
+ Descriptografar PDF
+ Crie uma cópia desbloqueada sem alterar o original.
+ Selecionar PDF protegido
+ Senha do PDF
+ Salvar cópia desbloqueada
+ PDF desbloqueado salvo.
+ A senha está incorreta.
+ Ver PDF
+ Ver PDF criptografado
+ Ver PDF desbloqueado
+ Criptografar PDF
+ Crie uma cópia protegida por senha sem alterar o original.
+ Selecionar PDF para proteger
+ Criar senha do PDF
+ Confirmar senha
+ Salvar cópia criptografada
+ PDF criptografado salvo.
+ Digite uma senha para proteger o PDF.
+ As senhas não coincidem.
+ Este PDF já está protegido por senha.
+ Este tipo de arquivo ainda não pode ser visualizado.
+ Visualização de texto
+ Abrir documento
+ Página
+ Ir
+ OCR (%1$d)
+ Selecione dois ou mais PDFs para mesclar em um
+ Escolha um modo e execute uma ação principal clara.
+ %1$d arquivos selecionados
+ Reordene os arquivos antes de mesclar. De cima para baixo = ordem final.
+ Selecione pelo menos 2 PDFs para continuar.
+ Adicionar arquivos
+ Todos
+ Ímpares
+ Pares
+ Crie com digitalizações, imagens ou páginas em branco
+ Digitalize ou adicione imagens para iniciar seu rascunho.
+ Página %1$d
+ Toque nas setas para reordenar
+ Criar um PDF em branco
+ Defina o número de páginas e gere instantaneamente.
+ %1$d páginas
+ Texto avançado para PDF
+ Use para notas rápidas. Para documentos refinados, prefira digitalizar/imagem.
+ Digite o conteúdo...
+ %1$d caracteres
+ Importar imagens
+ Páginas em branco
+ Texto avançado
+ Criar PDF do rascunho
+ Criar PDF em branco
+ Criar PDF de texto
+ Adicionar imagens
+ Ajustar ao A4
+ Tamanho original
+ Adicione fotos para criar um PDF.
+ Compartilhar texto
+ Nenhuma camada de texto neste PDF.
+ Escolha um PDF para extrair o texto.
+ Reordenar, girar e excluir páginas
+ Selecionar tudo
+ Girar
+ Arrastar
+ Ińicio
+ Ferramentas
+ Configurações
+ Fechar
+ Anterior
+ Próximo
+ Desfazer
+ Alterar pasta
+ Pasta selecionada
+ Salvar documento
+ Nome do arquivo
+ Documento.pdf
+ Digitalizar documento
+ Detecte bordas, recorte e melhore seus documentos como em um scanner profissional.
+ Digitalizar documento
+ Scanner indisponível: %1$s
+ Importar da galeria
+ Recursos
+ Detecção e recorte automáticos de bordas
+ Correção de perspectiva
+ Digitalização de documentos com várias páginas
+ Importar da galeria
+ Exportar como PDF
+ %1$d páginas digitalizadas
+ 1 página digitalizada
+ Digitalizar mais
+ Adicionar da galeria
+ Limpar tudo
+ Filtro
+ Original
+ Automático
+ Escala de cinza
+ P&B
+ Vívido
+ Salvando...
+ Salvar como PDF
+ PDF salvo com sucesso!
+ Falha ao salvar o PDF
+ Tamanho: %1$d KB
+ Prévia da página %1$d
+ Página %1$d
+ Remover página
+ Imagem %1$d
+ PDF
+ Word
+ Excel
+ PowerPoint
+ Imagens
+ TXT
+ Apoie o ClearPDF
+ O ClearPDF é um projeto de código aberto no GitHub.\nVocê gostaria de favoritar o projeto?
+ Sim, favoritar
+ Agora não
+ Não perguntar novamente
+ Algo deu errado. Tente novamente.
+ Não foi possível abrir este PDF.
+ Não foi possível salvar o PDF.
+ Selecione pelo menos 2 PDFs
+ %1$d arquivos mesclados -> %2$s\nSalvo em %3$s
+ Falha ao mesclar.
+ Dividido em %1$d páginas\nSalvo em %2$s
+ %1$d páginas extraídas\nSalvo em %2$s
+ Selecione pelo menos uma página para extrair
+ Falha ao dividir.
+ Falha ao extrair.
+ Adicione pelo menos uma imagem
+ PDF de %1$d páginas criado\nSalvo em %2$s
+ Falha na conversão.
+ Selecione pelo menos uma imagem
+ O PDF em branco precisa de pelo menos 1 página
+ Digite algum texto
+ Não foi possível criar o arquivo de saída.
+ Não foi possível ler as imagens.
+ %1$s criado (%2$d páginas)\nSalvo em %3$s
+ Falha ao criar o PDF.
+ Comprimido: %1$dKB -> %2$dKB (%3$d%% menor)\nSalvo em %4$s
+ Falha ao comprimir.
+ Nenhum texto selecionável encontrado. Este PDF pode ser uma digitalização ou imagem - tente o OCR do scanner.
+ Falha ao extrair o texto.
+ Não foi possível abrir o PDF.
+ Não é possível excluir todas as páginas.
+ %1$d páginas salvas\nSalvo em %2$s
+ Falha ao salvar.
+ Falha ao abrir o PDF.
+ Cópia editada salva como %1$s
+ Falha ao salvar o PDF editado.
+ Organizar páginas
+ Imagens -> PDF
+ O scanner está indisponível neste contexto.
+ Não foi possível iniciar o scanner.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6c092e9..2cefdd4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -17,18 +17,20 @@
Your fully offline document companion
Choose your language
Personalize your experience
+ Language
Open anything
PDFs, docs, sheets, slides, images & more
Powerful tools
Annotate, sign, search, and organize documents
Continue
Get Started
+ %1$d of %2$d
ClearPDF
PDF WORKSPACE
PDF work, simply elegant.
- Open, scan, and organize your files — privately on your device.
+ Open, scan, and organize your files — privately on your device.
Open PDF
Scan
Recents
@@ -84,7 +86,7 @@
Choose application language
About
Version 1.1.0
- Made by Chethan616 with ❤
+ Made by Chethan616 with ❤
Star on GitHub
ClearPDF is open source
Open Source Licenses
@@ -102,7 +104,7 @@
Save Edits
Sign
Find
- Search in document…
+ Search in document…
%1$d of %2$d results
No results
Previous
@@ -122,9 +124,13 @@
Done
New Sign
Replace
- Saving edited PDF…
+ Saving edited PDF…
+ Reset Zoom %1$d%%
Size
Jump to Page
+ Page
+ Go
+ OCR (%1$d)
Draw your signature
@@ -135,6 +141,10 @@
Medium
Thick
Heavy
+ Sign here
+ Saved Signatures
+ Name this signature
+ Save name
Annotate & Sign
@@ -143,9 +153,9 @@
Search text and select entire paragraphs
Powerful Tools
Merge, split, compress, and convert
- You\'re all set! 🎉
+ You\'re all set! 🎉
ClearPDF is ready to use.\nAll your documents, always private.
- 100% offline · No data collection · Open source
+ 100% offline · No data collection · Open source
Back
@@ -160,4 +170,192 @@
Dismiss
Not now
Don\'t ask again
+ Custom Directory
+ Default Directory
+ Downloads / ClearPDF
+ Licensed under the Apache License, Version 2.0.\nYou may obtain a copy at apache.org/licenses/LICENSE-2.0
+ by %1$s
+ English
+ Portuguese
+ Open
+ Share
+ Details
+ Remove
+ Share PDF
+ PDF
+ Long press for quick actions
+ File Info
+ Pages
+ Added
+ Location
+ Unknown
+ Just now
+ %1$d min ago
+ %1$d h ago
+ Yesterday
+ %1$d KB
+ %1$.1f MB
+ %1$d B
+ %1$d pages
+ Size
+ Decrypt PDF
+ Remove a PDF password
+ Encrypt PDF
+ Protect with a password
+ Enter PDF password
+ This document is protected. Enter its password to continue.
+ Password
+ Unlock PDF
+ That password didn\'t unlock the PDF. Try again.
+ This PDF is password protected.
+ Decrypt PDF
+ Create an unlocked copy without changing the original.
+ Select protected PDF
+ PDF password
+ Save unlocked copy
+ Unlocked PDF saved.
+ The password is incorrect.
+ View PDF
+ View encrypted PDF
+ View unlocked PDF
+ Encrypt PDF
+ Create a password-protected copy without changing the original.
+ Select PDF to protect
+ Create PDF password
+ Confirm password
+ Save encrypted copy
+ Encrypted PDF saved.
+ Enter a password to protect the PDF.
+ The passwords do not match.
+ This PDF is already password protected.
+ This file type cannot be previewed yet.
+ Text preview
+ Open document
+ Select two or more PDF files to merge into one
+ Choose a mode, then run one clear primary action.
+ %1$d files selected
+ Reorder files before merging. Top to bottom = output order.
+ Select at least 2 PDFs to continue.
+ Add Files
+ All
+ Odd
+ Even
+ Create with scans, images, or blank pages
+ Scan or add images to start your PDF draft.
+ Page %1$d
+ Tap arrows to reorder
+ Create a clean blank PDF
+ Set the number of pages and generate instantly.
+ %1$d pages
+ Advanced text to PDF
+ Use this for quick notes. For polished docs, prefer scan/image mode.
+ Type text content...
+ %1$d characters
+ Import images
+ Blank pages
+ Advanced text
+ Create PDF from draft
+ Create blank PDF
+ Create text PDF
+ Add images
+ Fit to A4
+ Original size
+ Add photos to build a PDF.
+ Share text
+ No text layer in this PDF.
+ Pick a PDF to pull its text.
+ Reorder, rotate & delete pages
+ Select all
+ Rotate
+ Drag
+ Home
+ Tools
+ Settings
+ Close
+ Previous
+ Next
+ Undo
+ Change folder
+ Selected folder
+ Save document
+ File name
+ Document.pdf
+ Scan document
+ Auto-detect edges, crop and enhance your documents like a pro scanner.
+ Scan document
+ Scanner unavailable: %1$s
+ Import from gallery
+ Features
+ Auto edge detection and cropping
+ Perspective correction
+ Multi-page document scanning
+ Import from gallery
+ Export as PDF
+ %1$d pages scanned
+ 1 page scanned
+ Scan more
+ Add from gallery
+ Clear all
+ Filter
+ Original
+ Auto
+ Grayscale
+ B&W
+ Vivid
+ Saving...
+ Save as PDF
+ PDF saved successfully!
+ Failed to save PDF
+ Size: %1$d KB
+ Preview page %1$d
+ Page %1$d
+ Remove page
+ Image %1$d
+ PDF
+ Word
+ Excel
+ PowerPoint
+ Images
+ TXT
+ Support ClearPDF
+ ClearPDF is open source on GitHub.\nWould you like to star the project?
+ Yes, star it
+ Not now
+ Do not ask again
+ Something went wrong. Please try again.
+ Could not open this PDF.
+ Could not save the PDF.
+ Select at least 2 PDFs
+ Merged %1$d files -> %2$s\nSaved to %3$s
+ Merge failed.
+ Split into %1$d pages\nSaved to %2$s
+ Extracted %1$d pages\nSaved to %2$s
+ Select at least one page to extract
+ Split failed.
+ Extract failed.
+ Add at least one image
+ Created %1$d-page PDF\nSaved to %2$s
+ Conversion failed.
+ Please select at least one image
+ Blank PDF needs at least 1 page
+ Please enter some text
+ Failed to create the output file.
+ Could not decode any images.
+ Created %1$s (%2$d pages)\nSaved to %3$s
+ Failed to create PDF.
+ Compressed: %1$dKB -> %2$dKB (%3$d%% smaller)\nSaved to %4$s
+ Compression failed.
+ No selectable text found. This PDF may be a scan or image - try the scanner OCR instead.
+ Text extraction failed.
+ Could not open PDF.
+ Cannot delete every page.
+ Saved %1$d pages\nSaved to %2$s
+ Save failed.
+ Failed to open PDF.
+ Saved edited copy as %1$s
+ Failed to save edited PDF.
+ Organize pages
+ Images -> PDF
+ Scanner is unavailable in this context.
+ Could not start the scanner.
diff --git a/pdf-core/src/main/java/com/kyant/pdfcore/security/PdfSecurityService.kt b/pdf-core/src/main/java/com/kyant/pdfcore/security/PdfSecurityService.kt
new file mode 100644
index 0000000..44a9c70
--- /dev/null
+++ b/pdf-core/src/main/java/com/kyant/pdfcore/security/PdfSecurityService.kt
@@ -0,0 +1,99 @@
+package com.kyant.pdfcore.security
+
+import android.content.Context
+import android.net.Uri
+import com.kyant.pdfcore.internal.PdfBox
+import com.tom_roush.pdfbox.pdmodel.PDDocument
+import com.tom_roush.pdfbox.pdmodel.encryption.AccessPermission
+import com.tom_roush.pdfbox.pdmodel.encryption.StandardProtectionPolicy
+import java.io.File
+
+/**
+ * Password operations for PDFs. The service keeps the original URI untouched and performs
+ * all parsing/writing on the caller's worker thread.
+ */
+object PdfSecurityService {
+
+ class PasswordRequiredException : IllegalStateException("PDF password required")
+
+ fun isPasswordProtected(context: Context, uri: Uri): Boolean {
+ PdfBox.ensureInitialized(context)
+ return try {
+ context.contentResolver.openInputStream(uri)?.use { stream ->
+ PDDocument.load(stream).use { document -> document.isEncrypted }
+ } ?: false
+ } catch (error: Exception) {
+ if (isPasswordError(error)) true else throw error
+ }
+ }
+
+ /** Creates an unlocked temporary PDF for the viewer without modifying the source. */
+ fun decryptToCache(context: Context, uri: Uri, password: String): File {
+ PdfBox.ensureInitialized(context)
+ val outputDirectory = File(context.cacheDir, "decrypted_pdfs").apply { mkdirs() }
+ val output = File(outputDirectory, "unlocked_${System.currentTimeMillis()}.pdf")
+ try {
+ context.contentResolver.openInputStream(uri)?.use { input ->
+ PDDocument.load(input, password).use { document ->
+ document.setAllSecurityToBeRemoved(true)
+ output.outputStream().use { document.save(it) }
+ }
+ } ?: throw IllegalStateException("Unable to read PDF")
+ } catch (error: Exception) {
+ output.delete()
+ if (isPasswordError(error)) throw PasswordRequiredException()
+ throw error
+ }
+ return output
+ }
+
+ /** Writes an unlocked copy to a user-selected destination URI. */
+ fun decryptToUri(context: Context, sourceUri: Uri, destinationUri: Uri, password: String) {
+ PdfBox.ensureInitialized(context)
+ try {
+ context.contentResolver.openInputStream(sourceUri)?.use { input ->
+ PDDocument.load(input, password).use { document ->
+ document.setAllSecurityToBeRemoved(true)
+ context.contentResolver.openOutputStream(destinationUri)?.use { output ->
+ document.save(output)
+ } ?: throw IllegalStateException("Unable to write PDF")
+ }
+ } ?: throw IllegalStateException("Unable to read PDF")
+ } catch (error: Exception) {
+ if (isPasswordError(error)) throw PasswordRequiredException()
+ throw error
+ }
+ }
+
+ /** Writes a password-protected copy to a user-selected destination URI. */
+ fun encryptToUri(context: Context, sourceUri: Uri, destinationUri: Uri, password: String) {
+ require(password.isNotBlank()) { "PDF password required" }
+ PdfBox.ensureInitialized(context)
+ try {
+ context.contentResolver.openInputStream(sourceUri)?.use { input ->
+ PDDocument.load(input).use { document ->
+ val permissions = AccessPermission().apply {
+ setCanPrint(true)
+ setCanModify(false)
+ setCanExtractContent(true)
+ }
+ val policy = StandardProtectionPolicy(password, password, permissions).apply {
+ encryptionKeyLength = 128
+ }
+ document.protect(policy)
+ context.contentResolver.openOutputStream(destinationUri)?.use { output ->
+ document.save(output)
+ } ?: throw IllegalStateException("Unable to write PDF")
+ }
+ } ?: throw IllegalStateException("Unable to read PDF")
+ } catch (error: Exception) {
+ if (isPasswordError(error)) throw PasswordRequiredException()
+ throw error
+ }
+ }
+
+ private fun isPasswordError(error: Throwable): Boolean {
+ return generateSequence(error) { it.cause }
+ .any { it::class.java.simpleName.contains("InvalidPassword", ignoreCase = true) }
+ }
+}