78-import androidx.compose.material3.Slider
79-import androidx.compose.material3.RangeSlider
80-import androidx.compose.material3.RadioButton
81-import androidx.compose.material3.TopAppBar
82-import androidx.compose.material3.TopAppBarDefaults
83-import androidx.compose.material3.Surface
84-import androidx.compose.material3.TextButton
85-import androidx.compose.material3.AlertDialog
86-import androidx.compose.material3.Checkbox
87-import androidx.compose.runtime.*
88:import androidx.compose.ui.res.stringResource
89-import com.kompact.R
90-import kotlinx.coroutines.Dispatchers
91-import kotlinx.coroutines.launch
92-import kotlinx.coroutines.withContext
93-import androidx.compose.ui.Alignment
94-import androidx.compose.ui.Modifier
95-import androidx.compose.ui.geometry.Offset
96-import androidx.compose.ui.graphics.graphicsLayer
97-import androidx.compose.ui.graphics.drawscope.Stroke
98-import androidx.compose.ui.input.pointer.pointerInput
--
182-    onPickDefaultDestination: () -> Unit,
183-    onClearDefaultDestination: () -> Unit,
184-    modifier: Modifier = Modifier,
185-) {
186-    val drawerState = rememberDrawerState(DrawerValue.Closed)
187-    val scope = rememberCoroutineScope()
188-    ModalNavigationDrawer(
189-        drawerState = drawerState,
190-        drawerContent = {
191-            ModalDrawerSheet {
192:                Text(stringResource(R.string.app_name), modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.headlineSmall)
193-                HorizontalDivider()
194-                NavigationDrawerItem(
195:                    label = { Text(stringResource(R.string.nav_home)) },
196-                    selected = state.currentScreen == AppScreen.HOME,
197-                    onClick = {
198-                        onNavigateToHome()
199-                        scope.launch { drawerState.close() }
200-                    },
201-                    icon = { Icon(Icons.Filled.Home, contentDescription = null) },
202-                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
203-                )
204-                NavigationDrawerItem(
205:                    label = { Text(stringResource(R.string.nav_settings)) },
206-                    selected = state.currentScreen == AppScreen.SETTINGS,
207-                    onClick = {
208-                        onNavigateToSettings()
209-                        scope.launch { drawerState.close() }
210-                    },
211-                    icon = { Icon(Icons.Filled.Settings, contentDescription = null) },
212-                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
213-                )
214-                NavigationDrawerItem(
215:                    label = { Text(stringResource(R.string.nav_about)) },
216-                    selected = state.currentScreen == AppScreen.ABOUT,
217-                    onClick = {
218-                        onNavigateToAbout()
219-                        scope.launch { drawerState.close() }
220-                    },
221-                    icon = { Icon(Icons.Filled.Info, contentDescription = null) },
222-                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
223-                )
224-                NavigationDrawerItem(
225:                    label = { Text(stringResource(R.string.nav_donate)) },
226-                    selected = state.currentScreen == AppScreen.DONATE,
227-                    onClick = {
228-                        onNavigateToDonate()
229-                        scope.launch { drawerState.close() }
230-                    },
231-                    icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
232-                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
233-                )
234-                NavigationDrawerItem(
235:                    label = { Text(stringResource(R.string.nav_contact_us)) },
236-                    selected = state.currentScreen == AppScreen.CONTACT,
237-                    onClick = {
238-                        onNavigateToContact()
239-                        scope.launch { drawerState.close() }
240-                    },
241-                    icon = { Icon(Icons.Filled.Email, contentDescription = null) },
242-                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
243-                )
244-                val uriHandler = LocalUriHandler.current
245-                NavigationDrawerItem(
246:                    label = { Text(stringResource(R.string.nav_report_issue)) },
247-                    selected = false,
248-                    onClick = {
249-                        uriHandler.openUri("https://git.naxod.com/luca/Kompact/issues")
250-                        scope.launch { drawerState.close() }
251-                    },
252-                    icon = { Icon(Icons.Filled.BugReport, contentDescription = null) },
253-                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
254-                )
255-            }
256-        }
257-    ) {
258-        Scaffold(
259-            topBar = {
260-                TopAppBar(
261-                    colors = TopAppBarDefaults.topAppBarColors(
262-                        containerColor = MaterialTheme.colorScheme.surface
263-                    ),
264-                    title = {
265-                        Column(horizontalAlignment = Alignment.Start) {
266:                            Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.titleLarge)
267-                            Text(
268:                                text = stringResource(R.string.subtitle),
269-                                style = MaterialTheme.typography.labelSmall,
270-                                color = MaterialTheme.colorScheme.onSurfaceVariant
271-                            )
272-                        }
273-                    },
274-                    navigationIcon = {
275-                        IconButton(onClick = { scope.launch { drawerState.open() } }) {
276-                            Icon(Icons.Filled.Menu, contentDescription = "Menu")
277-                        }
278-                    },
279-                    actions = {
280-                        IconButton(onClick = { onNavigateToDonate() }) {
281:                            Icon(Icons.Filled.Favorite, contentDescription = stringResource(R.string.nav_donate))
282-                        }
283-                    }
284-                )
285-            }
286-        ) { innerPadding ->
287-            when (state.currentScreen) {
288-                AppScreen.HOME -> HomeScreen(
289-                    state = state,
290-                    onPickFiles = onPickFiles,
291-                    onCompressClick = onCompressClick,
--
600-    onClearDefaultDestination: () -> Unit,
601-    modifier: Modifier = Modifier,
602-) {
603-    LazyColumn(
604-        modifier = modifier
605-            .fillMaxSize()
606-            .padding(horizontal = 16.dp),
607-        contentPadding = PaddingValues(vertical = 16.dp),
608-        verticalArrangement = Arrangement.spacedBy(16.dp)
609-    ) {
610:        item { Text(stringResource(R.string.nav_settings), style = MaterialTheme.typography.headlineMedium) }
611-        item {
612-            Card(modifier = Modifier.fillMaxWidth()) {
613-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
614:                    Text(stringResource(R.string.language), style = MaterialTheme.typography.titleMedium)
615-
616:                    val defaultLanguage = stringResource(R.string.default_text)
617-                    val locales = remember {
618-                        listOf(
619-                            "" to defaultLanguage,
620-                            "en" to "English",
621-                            "ca" to "Català",
622-                            "cs" to "Čeština",
623-                            "de" to "Deutsch",
624-                            "es" to "Español",
625-                            "fr" to "Français",
626-                            "hr" to "Hrvatski",
--
647-                            "ja" to "日本語",
648-                            "zh-CN" to "中文 (简体)",
649-                            "ko" to "한국어"
650-                        )
651-                    }
652-
653-                    var expanded by remember { mutableStateOf(false) }
654-                    val currentLocales = androidx.appcompat.app.AppCompatDelegate.getApplicationLocales()
655-                    val currentTags = currentLocales.toLanguageTags()
656-                    val currentLang = if (currentTags.isBlank()) "" else currentTags.split("-")[0]
657:                    val selectedText = locales.find { it.first == currentLang }?.second ?: stringResource(R.string.default_text)
658-                    val isRtl = currentLang in listOf("ar", "fa")
659-                    val layoutDir = if (isRtl) androidx.compose.ui.unit.LayoutDirection.Rtl else androidx.compose.ui.unit.LayoutDirection.Ltr
660-
661-                    androidx.compose.runtime.CompositionLocalProvider(androidx.compose.ui.platform.LocalLayoutDirection provides layoutDir) {
662-                        ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
663-                            OutlinedTextField(
664-                                value = selectedText,
665-                                onValueChange = {},
666-                                readOnly = true,
667-                                trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
--
683-                            }
684-                        }
685-                    }
686-                }
687-            }
688-        }
689-
690-        item {
691-            Card(modifier = Modifier.fillMaxWidth()) {
692-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
693:                    Text(stringResource(R.string.default_dest), style = MaterialTheme.typography.titleMedium)
694-                    Text(
695-                        text = defaultDestinationFolder?.let { getFolderDisplayName(Uri.parse(it)) } ?: defaultDownloadsLabel.ifBlank { "Default Dir" },
696-                        style = MaterialTheme.typography.bodyMedium
697-                    )
698-                    Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
699-                        Button(onClick = onPickDefaultDestination) {
700-                            Icon(Icons.Filled.Folder, contentDescription = null)
701-                            Spacer(Modifier.width(8.dp))
702:                            Text(stringResource(R.string.dest_folder))
703-                        }
704-                        TextButton(onClick = onClearDefaultDestination, enabled = defaultDestinationFolder != null) {
705:                            Text(stringResource(R.string.reset_default))
706-                        }
707-                    }
708-                }
709-            }
710-        }
711-
712-        item {
713-            Card(modifier = Modifier.fillMaxWidth()) {
714-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
715:                    Text(stringResource(R.string.theme_title), style = MaterialTheme.typography.titleMedium)
716-
717-                    @Composable
718-                    fun labelFor(mode: ThemeMode): String = when (mode) {
719:                        ThemeMode.SYSTEM -> stringResource(R.string.theme_system)
720:                        ThemeMode.LIGHT -> stringResource(R.string.theme_light)
721:                        ThemeMode.DARK -> stringResource(R.string.theme_dark)
722-                    }
723-
724-                    ThemeMode.entries.forEach { mode ->
725-                        Row(
726-                            modifier = Modifier
727-                                .fillMaxWidth()
728-                                .clickable { onThemeModeChange(mode) },
729-                            verticalAlignment = Alignment.CenterVertically,
730-                            horizontalArrangement = Arrangement.spacedBy(8.dp)
731-                        ) {
--
744-
745-@Composable
746-fun AboutScreen(modifier: Modifier = Modifier) {
747-    LazyColumn(
748-        modifier = modifier
749-            .fillMaxSize()
750-            .padding(horizontal = 16.dp),
751-        contentPadding = PaddingValues(vertical = 16.dp),
752-        verticalArrangement = Arrangement.spacedBy(16.dp)
753-    ) {
754:        item { Text(stringResource(R.string.about_title), style = MaterialTheme.typography.headlineMedium) }
755-        item {
756-            Card {
757-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
758:                    Text(stringResource(R.string.mission), style = MaterialTheme.typography.titleMedium)
759-                    Text(
760:                        stringResource(R.string.kompact_is_an_offline_file_compressor_designed_to_run_entirely_on_device_your_files_never_leave_your_phone_ensuring_complete_privacy),
761-                        style = MaterialTheme.typography.bodyMedium
762-                    )
763-                }
764-            }
765-        }
766-        item {
767-            Card {
768-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
769:                    Text(stringResource(R.string.current_features), style = MaterialTheme.typography.titleMedium)
770-                    Text(
771:                        stringResource(R.string.precision_compression_target_specific_file_sizes_or_quality_percentages_n) +
772-                        "• Format Conversion: Switch easily between JPG, PNG, WebP, and more.\n" +
773-                        "• Smart Editing: Crop images and manage EXIF metadata (view, edit, or strip) before processing.\n" +
774-                        "• Live Preview: Compare original vs. compressed results in real-time.",
775-                        style = MaterialTheme.typography.bodyMedium
776-                    )
777-                }
778-            }
779-        }
780-        item {
781-            Card {
782-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
783:                    Text(stringResource(R.string.roadmap), style = MaterialTheme.typography.titleMedium)
784-                    Text(
785:                        stringResource(R.string.we_are_working_to_make_kompact_the_ultimate_optimizer_future_updates_will_bring_support_for_pdf_documents_audio_tracks_and_video_files),
786-                        style = MaterialTheme.typography.bodyMedium
787-                    )
788-                }
789-            }
790-        }
791-        item {
792-            Card {
793-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
794:                    Text(stringResource(R.string.open_source), style = MaterialTheme.typography.titleMedium)
795-                    Text(
796:                        stringResource(R.string.this_project_is_open_source_under_the_apache_2_0_license_you_can_inspect_the_code_report_issues_or_contribute_on_our_repository),
797-                        style = MaterialTheme.typography.bodyMedium
798-                    )
799-                    val uriHandler = LocalUriHandler.current
800-                    TextButton(onClick = { uriHandler.openUri("https://git.naxod.com/luca/Kompact") }) {
801:                        Text(stringResource(R.string.visit_repository))
802-                    }
803-                }
804-            }
805-        }
806-    }
807-}
808-
809-@Composable
810-fun DonateScreen(modifier: Modifier = Modifier) {
811-    val uriHandler = androidx.compose.ui.platform.LocalUriHandler.current
812-    LazyColumn(
813-        modifier = modifier
814-            .fillMaxSize()
815-            .padding(horizontal = 16.dp),
816-        contentPadding = PaddingValues(vertical = 16.dp),
817-        verticalArrangement = Arrangement.spacedBy(16.dp)
818-    ) {
819:        item { Text(stringResource(R.string.support_development), style = MaterialTheme.typography.headlineMedium) }
820-        item {
821-            Card {
822-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
823:                    Text(stringResource(R.string.a_personal_note), style = MaterialTheme.typography.titleMedium)
824-                    Text(
825:                        stringResource(R.string.i_built_kompact_because_i_believe_essential_tools_should_be_simple_private_and_free_of_ads_or_tracking_i_work_on_this_in_my_spare_time_driven_by_the_idea_of_making_a_useful_utility_for_everyone),
826-                        style = MaterialTheme.typography.bodyMedium
827-                    )
828-                }
829-            }
830-        }
831-        item {
832-            Card {
833-                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
834:                    Text(stringResource(R.string.support), style = MaterialTheme.typography.titleMedium)
835-                    Text(
836:                        stringResource(R.string.if_kompact_has_saved_you_space_or_time_please_consider_supporting_its_development_your_donation_directly_helps_me_dedicate_more_hours_to_adding_features_like_pdf_audio_and_video_support_and_keeps_the_project_independent_and_sustainable),
837-                        style = MaterialTheme.typography.bodyMedium
838-                    )
839-                    Button(onClick = { uriHandler.openUri("https://liberapay.com/naxod/") }) {
840-                        Icon(Icons.Default.Favorite, contentDescription = null)
841-                        Spacer(Modifier.width(8.dp))
842:                        Text(stringResource(R.string.nav_donate))
843-                    }
844-                }
845-            }
846-        }
847-    }
848-}
849-
850-
851-@Composable
852-fun SourceCard(
--
858-    errorMessage: String?,
859-) {
860-    Card {
861-        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
862-            Row(
863-                modifier = Modifier.fillMaxWidth(),
864-                verticalAlignment = Alignment.CenterVertically,
865-                horizontalArrangement = Arrangement.spacedBy(12.dp)
866-            ) {
867-                Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
868:                    Text(stringResource(R.string.files), style = MaterialTheme.typography.titleMedium)
869-                    if (files.isNotEmpty()) {
870-                        Text(
871:                            text = "${files.size} ${stringResource(R.string.items_selected)}",
872-                            style = MaterialTheme.typography.bodySmall,
873-                            color = MaterialTheme.colorScheme.onSurfaceVariant
874-                        )
875-                    }
876-                }
877-                if (files.isNotEmpty()) {
878-                    OutlinedButton(onClick = onPickFiles) {
879-                        Icon(Icons.Default.CloudUpload, contentDescription = null)
880-                        Spacer(Modifier.width(8.dp))
881:                        Text(stringResource(R.string.add_more))
882-                    }
883-                }
884-                TextButton(
885-                    onClick = onClearSelection,
886-                    enabled = files.isNotEmpty()
887-                ) {
888:                    Text(stringResource(R.string.clear_all))
889-                }
890-            }
891-            if (errorMessage != null) {
892-                Text(errorMessage, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
893-            }
894-            if (selectionMessage != null) {
895-                Text(selectionMessage, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
896-            }
897-            if (files.isEmpty()) {
898-                SourceEmptyState(onPickFiles)
--
948-        modifier = Modifier
949-            .fillMaxWidth()
950-            .padding(vertical = 32.dp),
951-        horizontalAlignment = Alignment.CenterHorizontally,
952-        verticalArrangement = Arrangement.spacedBy(12.dp)
953-    ) {
954-        Icon(Icons.Default.Image, contentDescription = null, modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f))
955-        Button(onClick = onPickFiles) {
956-            Icon(Icons.Default.CloudUpload, contentDescription = null)
957-            Spacer(Modifier.width(8.dp))
958:            Text(stringResource(R.string.select_images_to_compress), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
959-        }
960-    }
961-}
962-
963-@Composable
964-fun PrimaryActionBar(
965-    isProcessing: Boolean,
966-    canCompress: Boolean,
967-    onCompressClick: () -> Unit,
968-    deleteOriginals: Boolean,
969-    onDeleteOriginalsChange: (Boolean) -> Unit,
970-    modifier: Modifier = Modifier,
971-) {
972-    var showDeleteConfirmDialog by remember { mutableStateOf(false) }
973-
974-    if (showDeleteConfirmDialog) {
975-        AlertDialog(
976-            onDismissRequest = { showDeleteConfirmDialog = false },
977:            title = { Text(stringResource(R.string.delete_originals_title)) },
978:            text = { Text(stringResource(R.string.delete_originals_message)) },
979-            confirmButton = {
980-                TextButton(
981-                    onClick = {
982-                        onDeleteOriginalsChange(true)
983-                        showDeleteConfirmDialog = false
984-                    }
985-                ) {
986:                    Text(stringResource(R.string.yes_delete))
987-                }
988-            },
989-            dismissButton = {
990-                TextButton(onClick = { showDeleteConfirmDialog = false }) {
991:                    Text(stringResource(R.string.cancel))
992-                }
993-            }
994-        )
995-    }
996-
997-    Surface(
998-        modifier = modifier.fillMaxWidth(),
999-        shape = RoundedCornerShape(20.dp),
1000-        tonalElevation = 2.dp,
1001-        border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
1002-    ) {
1003-        Row(
1004-            modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp),
1005-            verticalAlignment = Alignment.CenterVertically,
1006-            horizontalArrangement = Arrangement.spacedBy(16.dp)
1007-        ) {
1008-            Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
1009:                val processingText = stringResource(R.string.processing_dots)
1010:                val readyText = stringResource(R.string.ready_to_process)
1011-                Text(
1012-                    text = if (isProcessing) processingText else readyText,
1013-                    style = MaterialTheme.typography.titleMedium
1014-                )
1015-                Row(
1016-                    modifier = Modifier
1017-                        .fillMaxWidth()
1018-                        .padding(top = 8.dp)
1019-                        .clickable(enabled = !isProcessing) {
1020-                            if (deleteOriginals) {
--
1024-                            }
1025-                        },
1026-                    verticalAlignment = Alignment.CenterVertically
1027-                ) {
1028-                    Checkbox(
1029-                        checked = deleteOriginals,
1030-                        onCheckedChange = null,
1031-                        enabled = !isProcessing,
1032-                    )
1033-                    Text(
1034:                        text = stringResource(R.string.delete_original_files),
1035-                        style = MaterialTheme.typography.bodyMedium,
1036-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
1037-                    )
1038-                }
1039-            }
1040-            Button(
1041-                onClick = onCompressClick,
1042-                enabled = canCompress,
1043-                modifier = Modifier.height(48.dp)
1044-            ) {
1045-                if (isProcessing) {
1046-                    CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary)
1047-                    Spacer(Modifier.width(8.dp))
1048:                    Text(stringResource(R.string.working))
1049-                } else {
1050-                    Icon(Icons.Default.PlayArrow, contentDescription = null)
1051-                    Spacer(Modifier.width(8.dp))
1052:                    Text(stringResource(R.string.start))
1053-                }
1054-            }
1055-        }
1056-    }
1057-}
1058-
1059-@Composable
1060-fun OutputCard(
1061-    category: SourceCategory,
1062-    imageConfig: ImageCompressionConfig,
--
1075-    isEditingExif: Boolean,
1076-    onToggleExifEditor: () -> Unit,
1077-) {
1078-    Card {
1079-        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
1080-            Row(
1081-                modifier = Modifier.fillMaxWidth(),
1082-                horizontalArrangement = Arrangement.SpaceBetween,
1083-                verticalAlignment = Alignment.CenterVertically
1084-            ) {
1085:                Text(stringResource(R.string.output), style = MaterialTheme.typography.titleMedium)
1086-            }
1087-            if (category == SourceCategory.IMAGE) {
1088-                ImageConfigSection(
1089-                    config = imageConfig,
1090-                    onConfigChange = onImageConfigChange,
1091-                    defaultFormatLabel = defaultFormatLabel,
1092-                    originalFormat = originalFormat,
1093-                    defaultFileName = defaultFileName,
1094-                    referenceBytes = referenceBytes,
1095-                    liveEstimateBytes = liveEstimateBytes,
--
1099-            HorizontalDivider()
1100-            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
1101-                val destinationLabel = destinationFolder?.let { getFolderDisplayName(it) } ?: defaultDestinationLabel
1102-                OutlinedButton(
1103-                    onClick = onPickDestination,
1104-                    modifier = Modifier.weight(1f)
1105-                ) {
1106-                    Icon(Icons.Default.Folder, contentDescription = null)
1107-                    Spacer(Modifier.width(8.dp))
1108-                    Column {
1109:                        Text(stringResource(R.string.dest_folder), style = MaterialTheme.typography.labelSmall)
1110-                        Text(destinationLabel, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
1111-                    }
1112-                }
1113-                TextButton(onClick = onClearDestination, enabled = destinationFolder != null) {
1114:                    Text(stringResource(R.string.default_text))
1115-                }
1116-            }
1117-        }
1118-    }
1119-}
1120-
1121-@Composable
1122-@OptIn(ExperimentalMaterial3Api::class)
1123-fun ImageConfigSection(
1124-    config: ImageCompressionConfig,
1125-    onConfigChange: (ImageCompressionConfig) -> Unit,
1126-    defaultFormatLabel: String?,
1127-    originalFormat: ImageOutputFormat?,
1128-    defaultFileName: String?,
1129-    referenceBytes: Long,
1130-    liveEstimateBytes: Long?,
1131-    selectedFilesCount: Int,
1132-) {
1133-    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
1134:        Text(stringResource(R.string.format), style = MaterialTheme.typography.titleSmall)
1135-        var expanded by remember { mutableStateOf(false) }
1136-        val formatOptions = remember(originalFormat) {
1137-            ImageOutputFormat.entries.filterNot { format ->
1138-                originalFormat != null && format != ImageOutputFormat.ORIGINAL && format == originalFormat
1139-            }
1140-        }
1141-        ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
1142-            OutlinedTextField(
1143-                value = formatDisplayLabel(config.format, defaultFormatLabel),
1144-                onValueChange = {},
--
1151-                    DropdownMenuItem(
1152-                        text = { Text(formatDisplayLabel(format, defaultFormatLabel)) },
1153-                        onClick = {
1154-                            onConfigChange(config.copy(format = format))
1155-                            expanded = false
1156-                        }
1157-                    )
1158-                }
1159-            }
1160-        }
1161:        Text(stringResource(R.string.quality), style = MaterialTheme.typography.titleSmall)
1162-        val configPercentage = when (config.goal) {
1163-            CompressionGoal.PERCENTAGE -> config.percentage
1164-            CompressionGoal.TARGET_SIZE -> if (config.targetSizeBytes == null) config.percentage else (config.targetSizeBytes.toFloat().div(referenceBytes).times(100)).roundToInt()
1165-        }.coerceIn(0, 100)
1166-
1167-        var isDragging by remember { mutableStateOf(false) }
1168-        var sliderPercentage by remember { mutableIntStateOf(configPercentage) }
1169-        LaunchedEffect(configPercentage) {
1170-            if (!isDragging) {
1171-                sliderPercentage = configPercentage
--
1217-                            }
1218-                        }
1219-                    }
1220-                },
1221-                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
1222-                modifier = Modifier.width(80.dp)
1223-            )
1224-        }
1225-        if (liveEstimateBytes != null && selectedFilesCount > 0) {
1226-            val totalEstimate = liveEstimateBytes * selectedFilesCount
1227:            Text("${stringResource(R.string.estimated_total)} ${totalEstimate.asReadableBytes()}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
1228-        }
1229-        if (defaultFileName != null) {
1230-            // Keep a draft independent from preview recalculations; do not re-inject the default name once the user cleared it.
1231-            var fileNameDraft by rememberSaveable(defaultFileName) {
1232-                mutableStateOf(config.outputFileName ?: defaultFileName)
1233-            }
1234-
1235-            // If config explicitly carries a custom name, reflect it; otherwise leave the user draft untouched.
1236-            LaunchedEffect(config.outputFileName, defaultFileName) {
1237-                val desired = config.outputFileName
--
1239-                    fileNameDraft = desired
1240-                }
1241-            }
1242-
1243-            val focusManager = LocalFocusManager.current
1244-
1245-            // Update local draft immediately, but debounce applying to the shared config
1246-            OutlinedTextField(
1247-                value = fileNameDraft,
1248-                onValueChange = { fileNameDraft = it },
1249:                label = { Text(stringResource(R.string.output_filename)) },
1250-                modifier = Modifier.fillMaxWidth(),
1251-                keyboardOptions = KeyboardOptions(
1252-                    keyboardType = KeyboardType.Text,
1253-                    imeAction = ImeAction.Done
1254-                ),
1255-                keyboardActions = KeyboardActions(onDone = {
1256-                    val trimmed = fileNameDraft.trim()
1257-                    val normalized = trimmed.takeIf { it.isNotBlank() }
1258-                    if (normalized != config.outputFileName) {
1259-                        onConfigChange(config.copy(outputFileName = normalized))
--
1302-            Column(
1303-                modifier = Modifier
1304-                    .fillMaxWidth()
1305-                    .heightIn(max = 600.dp)
1306-                    .verticalScroll(scrollState)
1307-                    .padding(20.dp),
1308-                verticalArrangement = Arrangement.spacedBy(12.dp)
1309-            ) {
1310-                Row(verticalAlignment = Alignment.CenterVertically) {
1311-                    Text(
1312:                        text = stringResource(R.string.edit_exif_metadata),
1313-                        style = MaterialTheme.typography.titleMedium,
1314-                        modifier = Modifier.weight(1f)
1315-                    )
1316-                    IconButton(onClick = onDismiss) {
1317-                        Icon(Icons.Default.Close, contentDescription = "Close EXIF editor")
1318-                    }
1319-                }
1320-                Text(
1321:                    text = stringResource(R.string.changes_apply_instantly_to_the_selected_image),
1322-                    style = MaterialTheme.typography.bodySmall,
1323-                    color = MaterialTheme.colorScheme.onSurfaceVariant
1324-                )
1325-
1326:                Text(stringResource(R.string.basic_info), style = MaterialTheme.typography.titleSmall)
1327-                OutlinedTextField(
1328-                    value = currentExif.make ?: "",
1329-                    onValueChange = { onExifDataChange(currentExif.copy(make = it.takeIf { it.isNotBlank() })) },
1330:                    label = { Text(stringResource(R.string.make)) },
1331-                    modifier = Modifier.fillMaxWidth()
1332-                )
1333-                OutlinedTextField(
1334-                    value = currentExif.model ?: "",
1335-                    onValueChange = { onExifDataChange(currentExif.copy(model = it.takeIf { it.isNotBlank() })) },
1336:                    label = { Text(stringResource(R.string.model)) },
1337-                    modifier = Modifier.fillMaxWidth()
1338-                )
1339-                OutlinedTextField(
1340-                    value = currentExif.software ?: "",
1341-                    onValueChange = { onExifDataChange(currentExif.copy(software = it.takeIf { it.isNotBlank() })) },
1342:                    label = { Text(stringResource(R.string.software)) },
1343-                    modifier = Modifier.fillMaxWidth()
1344-                )
1345-
1346:                Text(stringResource(R.string.date_and_time), style = MaterialTheme.typography.titleSmall)
1347-                OutlinedTextField(
1348-                    value = currentExif.dateTime ?: "",
1349-                    onValueChange = { onExifDataChange(currentExif.copy(dateTime = it.takeIf { it.isNotBlank() })) },
1350:                    label = { Text(stringResource(R.string.date_time)) },
1351-                    modifier = Modifier.fillMaxWidth()
1352-                )
1353-
1354:                Text(stringResource(R.string.location), style = MaterialTheme.typography.titleSmall)
1355-                OutlinedTextField(
1356-                    value = currentExif.latitude?.toString() ?: "",
1357-                    onValueChange = { onExifDataChange(currentExif.copy(latitude = it.toDoubleOrNull())) },
1358:                    label = { Text(stringResource(R.string.lat)) },
1359-                    modifier = Modifier.fillMaxWidth()
1360-                )
1361-                OutlinedTextField(
1362-                    value = currentExif.longitude?.toString() ?: "",
1363-                    onValueChange = { onExifDataChange(currentExif.copy(longitude = it.toDoubleOrNull())) },
1364:                    label = { Text(stringResource(R.string.lon)) },
1365-                    modifier = Modifier.fillMaxWidth()
1366-                )
1367-                OutlinedTextField(
1368-                    value = currentExif.altitude?.toString() ?: "",
1369-                    onValueChange = { onExifDataChange(currentExif.copy(altitude = it.toDoubleOrNull())) },
1370:                    label = { Text(stringResource(R.string.alt)) },
1371-                    modifier = Modifier.fillMaxWidth()
1372-                )
1373-
1374:                Text(stringResource(R.string.camera_settings), style = MaterialTheme.typography.titleSmall)
1375-                OutlinedTextField(
1376-                    value = currentExif.exposureTime ?: "",
1377-                    onValueChange = { onExifDataChange(currentExif.copy(exposureTime = it.takeIf { it.isNotBlank() })) },
1378:                    label = { Text(stringResource(R.string.exp_time)) },
1379-                    modifier = Modifier.fillMaxWidth()
1380-                )
1381-                OutlinedTextField(
1382-                    value = currentExif.fNumber ?: "",
1383-                    onValueChange = { onExifDataChange(currentExif.copy(fNumber = it.takeIf { it.isNotBlank() })) },
1384:                    label = { Text(stringResource(R.string.f)) },
1385-                    modifier = Modifier.fillMaxWidth()
1386-                )
1387-                OutlinedTextField(
1388-                    value = currentExif.iso?.toString() ?: "",
1389-                    onValueChange = { onExifDataChange(currentExif.copy(iso = it.toIntOrNull())) },
1390:                    label = { Text(stringResource(R.string.iso)) },
1391-                    modifier = Modifier.fillMaxWidth()
1392-                )
1393-                OutlinedTextField(
1394-                    value = currentExif.focalLength ?: "",
1395-                    onValueChange = { onExifDataChange(currentExif.copy(focalLength = it.takeIf { it.isNotBlank() })) },
1396:                    label = { Text(stringResource(R.string.focal_len)) },
1397-                    modifier = Modifier.fillMaxWidth()
1398-                )
1399-                OutlinedTextField(
1400-                    value = currentExif.flash?.toString() ?: "",
1401-                    onValueChange = { onExifDataChange(currentExif.copy(flash = it.toIntOrNull())) },
1402:                    label = { Text(stringResource(R.string.flash)) },
1403-                    modifier = Modifier.fillMaxWidth()
1404-                )
1405-                OutlinedTextField(
1406-                    value = currentExif.whiteBalance?.toString() ?: "",
1407-                    onValueChange = { onExifDataChange(currentExif.copy(whiteBalance = it.toIntOrNull())) },
1408:                    label = { Text(stringResource(R.string.wb)) },
1409-                    modifier = Modifier.fillMaxWidth()
1410-                )
1411-                OutlinedTextField(
1412-                    value = currentExif.orientation?.toString() ?: "",
1413-                    onValueChange = { onExifDataChange(currentExif.copy(orientation = it.toIntOrNull())) },
1414:                    label = { Text(stringResource(R.string.orientation)) },
1415-                    modifier = Modifier.fillMaxWidth()
1416-                )
1417-
1418:                Text(stringResource(R.string.metadata), style = MaterialTheme.typography.titleSmall)
1419-                OutlinedTextField(
1420-                    value = currentExif.artist ?: "",
1421-                    onValueChange = { onExifDataChange(currentExif.copy(artist = it.takeIf { it.isNotBlank() })) },
1422:                    label = { Text(stringResource(R.string.artist)) },
1423-                    modifier = Modifier.fillMaxWidth()
1424-                )
1425-                OutlinedTextField(
1426-                    value = currentExif.copyright ?: "",
1427-                    onValueChange = { onExifDataChange(currentExif.copy(copyright = it.takeIf { it.isNotBlank() })) },
1428:                    label = { Text(stringResource(R.string.copyright)) },
1429-                    modifier = Modifier.fillMaxWidth()
1430-                )
1431-                OutlinedTextField(
1432-                    value = currentExif.description ?: "",
1433-                    onValueChange = { onExifDataChange(currentExif.copy(description = it.takeIf { it.isNotBlank() })) },
1434:                    label = { Text(stringResource(R.string.description)) },
1435-                    modifier = Modifier.fillMaxWidth()
1436-                )
1437-                OutlinedTextField(
1438-                    value = currentExif.userComment ?: "",
1439-                    onValueChange = { onExifDataChange(currentExif.copy(userComment = it.takeIf { it.isNotBlank() })) },
1440:                    label = { Text(stringResource(R.string.user_comment)) },
1441-                    modifier = Modifier.fillMaxWidth()
1442-                )
1443-
1444-                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
1445-                    Row(
1446-                        modifier = Modifier.fillMaxWidth(),
1447-                        horizontalArrangement = Arrangement.spacedBy(8.dp),
1448-                        verticalAlignment = Alignment.CenterVertically
1449-                    ) {
1450-                        OutlinedButton(onClick = { onExifDataChange(ExifData()) }) {
1451:                            Text(stringResource(R.string.remove_metadata))
1452-                        }
1453-                        Spacer(modifier = Modifier.weight(1f))
1454-                        TextButton(onClick = onDismiss) {
1455:                            Text(stringResource(R.string.done))
1456-                        }
1457-                    }
1458-                    Button(onClick = { onExifDataChange(originalExifData) }, enabled = originalExifData != null, modifier = Modifier.fillMaxWidth()) {
1459:                        Text(stringResource(R.string.restore_original))
1460-                    }
1461-                }
1462-            }
1463-        }
1464-    }
1465-}
1466-
1467-@Composable
1468-@OptIn(ExperimentalMaterial3Api::class)
1469-fun CropEditorDialog(
--
1535-        ) {
1536-            Column(
1537-                modifier = Modifier
1538-                    .fillMaxWidth()
1539-                    .verticalScroll(rememberScrollState())
1540-                    .padding(20.dp),
1541-                verticalArrangement = Arrangement.spacedBy(16.dp)
1542-            ) {
1543-                Row(verticalAlignment = Alignment.CenterVertically) {
1544-                    Text(
1545:                        text = stringResource(R.string.crop_image),
1546-                        style = MaterialTheme.typography.titleMedium,
1547-                        modifier = Modifier.weight(1f)
1548-                    )
1549-                    IconButton(onClick = onDismiss) {
1550-                        Icon(Icons.Default.Close, contentDescription = "Close crop editor")
1551-                    }
1552-                }
1553-                Text(
1554:                    text = stringResource(R.string.drag_the_guides_or_use_the_sliders_to_define_what_stays_before_compression),
1555-                    style = MaterialTheme.typography.bodySmall,
1556-                    color = MaterialTheme.colorScheme.onSurfaceVariant
1557-                )
1558-
1559-                if (imageUri != null) {
1560-                    val previewPadding = 2.dp
1561-                    val sliderGap = 2.dp
1562-                    val sliderWidth = 100.dp
1563-                    val previewShape = RoundedCornerShape(20.dp)
1564-                    BoxWithConstraints(
--
1851-                                    text = "${(verticalRange.start * 100).roundToInt()}% - ${(verticalRange.endInclusive * 100).roundToInt()}%",
1852-                                    style = MaterialTheme.typography.bodySmall,
1853-                                    color = MaterialTheme.colorScheme.onSurfaceVariant,
1854-                                    textAlign = TextAlign.Center
1855-                                )
1856-                            }
1857-                        }
1858-                    }
1859-                } else {
1860-                    Text(
1861:                        text = stringResource(R.string.select_an_image_to_crop),
1862-                        style = MaterialTheme.typography.bodyMedium,
1863-                        color = MaterialTheme.colorScheme.onSurfaceVariant
1864-                    )
1865-                }
1866-
1867-                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
1868-                    RangeSlider(
1869-                        value = horizontalRange,
1870-                        onValueChange = { range ->
1871-                            val start = range.start.coerceIn(0f, 1f - minSpan)
--
1908-                            Text(label)
1909-                        }
1910-                    }
1911-                }
1912-
1913-                Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
1914-                    TextButton(onClick = {
1915-                        horizontalRange = 0f..1f
1916-                        verticalRange = 0f..1f
1917-                    }) {
1918:                        Text(stringResource(R.string.reset))
1919-                    }
1920-                    Spacer(Modifier.weight(1f))
1921-                    TextButton(onClick = onDismiss) {
1922:                        Text(stringResource(R.string.cancel))
1923-                    }
1924-                    Button(onClick = {
1925-                        val normalized = currentBounds.normalized()
1926-                        val applied = normalized.takeUnless { it.isFull() }
1927-                        onCropBoundsChange(applied)
1928-                        onDismiss()
1929-                    }) {
1930:                        Text(stringResource(R.string.apply))
1931-                    }
1932-                }
1933-            }
1934-        }
1935-    }
1936-}
1937-
1938-@Composable
1939-private fun VerticalRangeSlider(
1940-    value: ClosedFloatingPointRange<Float>,
--
2002-            }
2003-        }
2004-    }
2005-    return aspectRatio
2006-}
2007-
2008-@Composable
2009-fun ProgressCard(state: CompressionUiState) {
2010-    val progress = if (state.totalCount == 0) 0f else state.processedCount.toFloat() / max(1, state.totalCount)
2011-    val context = LocalContext.current
2012:    val processingLabel = stringResource(R.string.processing_dots)
2013:    val completedLabel = stringResource(R.string.completed)
2014-    val title = if (state.isProcessing) processingLabel else completedLabel
2015-    
2016-    Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) {
2017-        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
2018-            Text(title, style = MaterialTheme.typography.titleMedium)
2019-            LinearProgressIndicator(progress = { progress.coerceIn(0f, 1f) }, modifier = Modifier.fillMaxWidth())
2020:            val fileSingular = stringResource(R.string.file_singular)
2021:            val filePlural = stringResource(R.string.file_plural)
2022:            val processedLabel = stringResource(R.string.processed)
2023-            val fileLabel = if (state.totalCount == 1) fileSingular else filePlural
2024-            Text("${state.processedCount}/${state.totalCount} $fileLabel $processedLabel", style = MaterialTheme.typography.bodySmall)
2025-            state.lastResult?.let { last ->
2026-                val lastItemName = remember(last.source) {
2027-                    DocumentFile.fromSingleUri(context, last.source)?.name ?: last.source.lastPathSegment ?: ""
2028-                }
2029:                val lastItemLabel = stringResource(R.string.last_item)
2030-                Text("$lastItemLabel: $lastItemName", style = MaterialTheme.typography.labelSmall)
2031-                Text(last.message, style = MaterialTheme.typography.labelSmall)
2032-            }
2033-        }
2034-    }
2035-}
2036-
2037-@Composable
2038-fun ResultsCard(results: List<CompressionResult>) {
2039-    val successfulResults = results.filter { it.success }
2040-    val totalOriginal = successfulResults.sumOf { it.originalSizeBytes }
2041-    val totalCompressed = successfulResults.sumOf { it.compressedSizeBytes }
2042-    var zoomTarget by remember { mutableStateOf<Uri?>(null) }
2043-    var zoomLabel by remember { mutableStateOf<String?>(null) }
2044-
2045-    Card {
2046-        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
2047:            Text(stringResource(R.string.results), style = MaterialTheme.typography.titleMedium)
2048-            if (successfulResults.isNotEmpty()) {
2049:                Text("${stringResource(R.string.total_label)} ${totalOriginal.asReadableBytes()} → ${totalCompressed.asReadableBytes()}", style = MaterialTheme.typography.bodyMedium)
2050-            }
2051-            LazyColumn(
2052-                modifier = Modifier.heightIn(max = 400.dp),
2053-                verticalArrangement = Arrangement.spacedBy(8.dp)
2054-            ) {
2055-                items(results.size) { index ->
2056-                    val result = results[index]
2057-                    ResultRow(
2058-                        result = result,
2059-                        onPreviewClick = { uri, label ->
--
2090-                            .fillMaxWidth()
2091-                            .heightIn(min = 200.dp, max = 420.dp)
2092-                            .clip(RoundedCornerShape(12.dp))
2093-                    ) {
2094-                        ZoomableImage(uri = zoomTarget)
2095-                    }
2096-                    TextButton(onClick = {
2097-                        zoomTarget = null
2098-                        zoomLabel = null
2099-                    }, modifier = Modifier.align(Alignment.End)) {
2100:                        Text(stringResource(R.string.close))
2101-                    }
2102-                }
2103-            }
2104-        }
2105-    }
2106-}
2107-
2108-@Composable
2109-fun ResultRow(
2110-    result: CompressionResult,
--
2117-    }
2118-    val sourceName = remember(result.source) {
2119-        DocumentFile.fromSingleUri(context, result.source)?.name ?: result.source.lastPathSegment ?: "File"
2120-    }
2121-    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
2122-        Text(sourceName, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
2123-        if (showPreview) {
2124-            Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
2125-                // Before
2126-                Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
2127:                    Text(stringResource(R.string.before), style = MaterialTheme.typography.bodySmall)
2128-                    AsyncImage(
2129-                        model = result.source,
2130-                        contentDescription = null,
2131-                        modifier = Modifier
2132-                            .size(80.dp)
2133-                            .clip(RoundedCornerShape(4.dp))
2134-                            .clickable { onPreviewClick(result.source, "Original") },
2135-                        contentScale = ContentScale.Crop
2136-                    )
2137-                    Text(result.originalSizeBytes.asReadableBytes(), style = MaterialTheme.typography.bodySmall)
2138-                }
2139-                // After
2140-                Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
2141:                    Text(stringResource(R.string.after), style = MaterialTheme.typography.bodySmall)
2142-                    AsyncImage(
2143-                        model = result.output,
2144-                        contentDescription = null,
2145-                        modifier = Modifier
2146-                            .size(80.dp)
2147-                            .clip(RoundedCornerShape(4.dp))
2148-                            .clickable { result.output?.let { onPreviewClick(it, "Compressed") } },
2149-                        contentScale = ContentScale.Crop
2150-                    )
2151-                    Text(result.compressedSizeBytes.asReadableBytes(), style = MaterialTheme.typography.bodySmall)
--
2237-    Card(modifier) {
2238-        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
2239-            Column(
2240-                verticalArrangement = Arrangement.spacedBy(8.dp)
2241-            ) {
2242-                Row(
2243-                    modifier = Modifier.fillMaxWidth(),
2244-                    horizontalArrangement = Arrangement.SpaceBetween,
2245-                    verticalAlignment = Alignment.CenterVertically
2246-                ) {
2247:                    Text(stringResource(R.string.live_preview), style = MaterialTheme.typography.titleMedium)
2248-                    TextButton(
2249-                        onClick = onResetEditsClick,
2250-                        colors = androidx.compose.material3.ButtonDefaults.textButtonColors(
2251-                            contentColor = MaterialTheme.colorScheme.onSurfaceVariant
2252-                        )
2253-                    ) {
2254-                        androidx.compose.material3.Icon(
2255-                            imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
2256:                            contentDescription = stringResource(R.string.reset),
2257-                            modifier = Modifier.size(16.dp)
2258-                        )
2259-                        Spacer(modifier = Modifier.width(4.dp))
2260:                        Text(stringResource(R.string.reset))
2261-                    }
2262-                }
2263-                @OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class)
2264-                androidx.compose.foundation.layout.FlowRow(
2265-                    modifier = Modifier.fillMaxWidth(),
2266-                    horizontalArrangement = Arrangement.spacedBy(8.dp)
2267-                ) {
2268-                    if (canCrop) {
2269-                        TextButton(onClick = onCropClick) {
2270-                            Icon(androidx.compose.material.icons.Icons.Default.Crop, contentDescription = null)
--
2277-                            Text(label)
2278-                        }
2279-                    }
2280-                    if (canResize) {
2281-                        TextButton(onClick = onResizeClick) {
2282-                            Icon(androidx.compose.material.icons.Icons.Default.PhotoSizeSelectLarge, contentDescription = null)
2283-                            Spacer(Modifier.width(8.dp))
2284-                            val label = when {
2285-                                isResizeEditorVisible -> "Close Resize"
2286-                                isResizeApplied -> "Edit Resize"
2287:                                else -> stringResource(R.string.resize)
2288-                            }
2289-                            Text(label)
2290-                        }
2291-                    }
2292-                    TextButton(onClick = onRotateClick) {
2293-                        Icon(androidx.compose.material.icons.Icons.AutoMirrored.Filled.RotateRight, contentDescription = null)
2294-                        Spacer(Modifier.width(8.dp))
2295:                        Text(stringResource(R.string.rotate))
2296-                    }
2297-                    TextButton(onClick = onColorFilterClick) {
2298-                        Icon(androidx.compose.material.icons.Icons.Default.Palette, contentDescription = null)
2299-                        Spacer(Modifier.width(8.dp))
2300:                        Text(stringResource(R.string.color_filter))
2301-                    }
2302-                    if (exifEditingEnabled) {
2303-                        TextButton(onClick = onToggleExifEditor) {
2304-                            Icon(androidx.compose.material.icons.Icons.Default.Settings, contentDescription = null)
2305-                            Spacer(Modifier.width(8.dp))
2306-                            Text("EXIF")
2307-                        }
2308-                    }
2309-                    TextButton(
2310-                        onClick = onTextClick,
--
2323-            }
2324-            val hasPreviews = livePreview.kind == PreviewKind.IMAGE && livePreview.previews.isNotEmpty()
2325-            val showLoadingOnly = livePreview.isGenerating && !hasPreviews
2326-
2327-            Box {
2328-                when {
2329-                    hasPreviews -> {
2330-                        val totalOriginal = livePreview.previews.sumOf { it.originalBytes }
2331-                        val totalEstimated = livePreview.previews.sumOf { it.estimatedBytes }
2332-                        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
2333:                            Text("${stringResource(R.string.total_label)} ${totalOriginal.asReadableBytes()} → ${totalEstimated.asReadableBytes()}", style = MaterialTheme.typography.bodyMedium)
2334-                            LazyColumn(
2335-                                modifier = Modifier.heightIn(max = 400.dp),
2336-                                verticalArrangement = Arrangement.spacedBy(8.dp)
2337-                            ) {
2338-                                items(livePreview.previews.size) { index ->
2339-                                    val preview = livePreview.previews[index]
2340-                                    LivePreviewRow(
2341-                                        preview = preview,
2342-                                        onPreviewClick = { uri, label ->
2343-                                            zoomTarget = uri
--
2348-                            }
2349-                        }
2350-                    }
2351-                    livePreview.errorMessage != null -> {
2352-                        Text(livePreview.errorMessage, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error)
2353-                    }
2354-                    showLoadingOnly -> {
2355-                        Box(Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {
2356-                            Column(horizontalAlignment = Alignment.CenterHorizontally) {
2357-                                CircularProgressIndicator()
2358:                                Text(stringResource(R.string.generating_preview), style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(top = 8.dp))
2359-                            }
2360-                        }
2361-                    }
2362-                }
2363-
2364-                if (livePreview.isGenerating && hasPreviews) {
2365-                    Box(
2366-                        Modifier
2367-                            .matchParentSize()
2368-                            .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.82f)),
2369-                        contentAlignment = Alignment.Center
2370-                    ) {
2371-                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
2372-                            CircularProgressIndicator()
2373:                            Text(stringResource(R.string.updating_preview), style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(top = 8.dp))
2374-                        }
2375-                    }
2376-                }
2377-            }
2378-        }
2379-    }
2380-    if (zoomTarget != null) {
2381-        androidx.compose.ui.window.Dialog(onDismissRequest = {
2382-            zoomTarget = null
2383-            zoomLabel = null
--
2398-                            .fillMaxWidth()
2399-                            .heightIn(min = 200.dp, max = 420.dp)
2400-                            .clip(RoundedCornerShape(12.dp))
2401-                    ) {
2402-                        ZoomableImage(uri = zoomTarget)
2403-                    }
2404-                    TextButton(onClick = {
2405-                        zoomTarget = null
2406-                        zoomLabel = null
2407-                    }, modifier = Modifier.align(Alignment.End)) {
2408:                        Text(stringResource(R.string.close))
2409-                    }
2410-                }
2411-            }
2412-        }
2413-    }
2414-}
2415-
2416-@Composable
2417-fun LivePreviewRow(
2418-    preview: ImagePreviewPayload,
2419-    onPreviewClick: (Uri, String) -> Unit,
2420-) {
2421-    Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) {
2422-        // Before
2423-        Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
2424:            Text(stringResource(R.string.before), style = MaterialTheme.typography.bodySmall)
2425-            AsyncImage(
2426-                model = preview.source,
2427-                contentDescription = null,
2428-                modifier = Modifier
2429-                    .size(80.dp)
2430-                    .clip(RoundedCornerShape(4.dp))
2431-                    .clickable { onPreviewClick(preview.source, "Original") },
2432-                contentScale = ContentScale.Crop
2433-            )
2434-            Text(preview.originalBytes.asReadableBytes(), style = MaterialTheme.typography.bodySmall)
2435-        }
2436-
2437-        // Cropped (Optional)
2438-        if (preview.croppedPreviewUri != null) {
2439:            val strCropped = stringResource(R.string.cropped)
2440-            Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
2441:                Text(stringResource(R.string.cropped), style = MaterialTheme.typography.bodySmall)
2442-                val context = androidx.compose.ui.platform.LocalContext.current
2443-                val croppedRequest = remember(preview) {
2444-                    coil.request.ImageRequest.Builder(context)
2445-                        .data(preview.croppedPreviewUri)
2446-                        .memoryCacheKey("crop_${System.identityHashCode(preview)}")
2447-                        .diskCacheKey("crop_${System.identityHashCode(preview)}")
2448-                        .build()
2449-                }
2450-                AsyncImage(
2451-                    model = croppedRequest,
--
2455-                        .clip(RoundedCornerShape(4.dp))
2456-                        .clickable { onPreviewClick(preview.croppedPreviewUri, strCropped) },
2457-                    contentScale = ContentScale.Crop
2458-                )
2459-                Text("-", style = MaterialTheme.typography.bodySmall)
2460-            }
2461-        }
2462-
2463-        // After
2464-        Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
2465:            Text(stringResource(R.string.after), style = MaterialTheme.typography.bodySmall)
2466-            val context = androidx.compose.ui.platform.LocalContext.current
2467-            val request = remember(preview) {
2468-                coil.request.ImageRequest.Builder(context)
2469-                    .data(preview.previewUri)
2470-                    .memoryCacheKey("after_${System.identityHashCode(preview)}")
2471-                    .diskCacheKey("after_${System.identityHashCode(preview)}")
2472-                    .build()
2473-            }
2474-            AsyncImage(
2475-                model = request,
--
2632-            shape = RoundedCornerShape(16.dp)
2633-        ) {
2634-            Column(
2635-                modifier = Modifier.padding(16.dp).verticalScroll(androidx.compose.foundation.rememberScrollState())
2636-            ) {
2637-                Row(
2638-                    modifier = Modifier.fillMaxWidth(),
2639-                    horizontalArrangement = Arrangement.SpaceBetween,
2640-                    verticalAlignment = Alignment.CenterVertically
2641-                ) {
2642:                    Text(stringResource(R.string.resize), style = MaterialTheme.typography.titleLarge)
2643-                    androidx.compose.material3.IconButton(onClick = onDismiss) {
2644-                        Icon(androidx.compose.material.icons.Icons.Default.Close, contentDescription = "Close")
2645-                    }
2646-                }
2647-                Spacer(Modifier.height(16.dp))
2648-
2649-                Box(
2650-                    modifier = Modifier
2651-                        .heightIn(max = maxImageHeight)
2652-                        .fillMaxWidth(),
2653-                    contentAlignment = Alignment.Center
2654-                ) {
2655-                    if (previewUri != null) {
2656-                        coil.compose.AsyncImage(
2657-                            model = previewUri,
2658-                            contentDescription = "Preview",
2659-                            modifier = Modifier.fillMaxSize(),
2660-                            contentScale = ContentScale.Fit
2661-                        )
2662-                    } else {
2663:                        Text(stringResource(R.string.no_preview_available))
2664-                    }
2665-                }
2666-
2667-                Spacer(Modifier.height(16.dp))
2668-                
2669-                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
2670-                    // Slider percentage
2671-                    Column(modifier = Modifier.fillMaxWidth().alpha(if (isPercentage) 1f else 0.5f)) {
2672:                        Text("${stringResource(R.string.scale_label)} ${currentPct.toInt()}%", style = MaterialTheme.typography.bodyMedium)
2673-                        Slider(
2674-                            value = currentPct,
2675-                            onValueChange = { 
2676-                                if (isPercentage) {
2677-                                    currentPct = it
2678-                                    imageDimensions?.let { dims ->
2679-                                        widthStr = (dims.width * (it / 100f)).roundToInt().toString()
2680-                                        heightStr = (dims.height * (it / 100f)).roundToInt().toString()
2681-                                    }
2682-                                }
--
2693-                            value = widthStr,
2694-                            onValueChange = { 
2695-                                isPercentage = false // Lock percentage
2696-                                widthStr = it 
2697-                                if (maintainAspect && aspectRatio != null && it.isNotEmpty()) {
2698-                                    it.toIntOrNull()?.let { w ->
2699-                                        heightStr = (w / aspectRatio).roundToInt().toString()
2700-                                    }
2701-                                }
2702-                            },
2703:                            label = { Text(stringResource(R.string.resize_width)) },
2704-                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
2705-                            modifier = Modifier.weight(1f)
2706-                        )
2707-                        OutlinedTextField(
2708-                            value = heightStr,
2709-                            onValueChange = { 
2710-                                isPercentage = false // Lock percentage
2711-                                heightStr = it 
2712-                                if (maintainAspect && aspectRatio != null && it.isNotEmpty()) {
2713-                                    it.toIntOrNull()?.let { h ->
2714-                                        widthStr = (h * aspectRatio).roundToInt().toString()
2715-                                    }
2716-                                }
2717-                            },
2718:                            label = { Text(stringResource(R.string.resize_height)) },
2719-                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
2720-                            modifier = Modifier.weight(1f)
2721-                        )
2722-                    }
2723-                    
2724-                    Row(verticalAlignment = Alignment.CenterVertically) {
2725-                        Checkbox(checked = maintainAspect, onCheckedChange = { maintainAspect = it })
2726:                        Text(stringResource(R.string.resize_keep_aspect))
2727-                    }
2728-                }
2729-
2730-                Spacer(Modifier.height(16.dp))
2731-                Row(modifier = Modifier.fillMaxWidth()) {
2732-                    TextButton(onClick = {
2733-                        isPercentage = true
2734-                        currentPct = 100f
2735-                        maintainAspect = true
2736-                        widthStr = imageDimensions?.width?.toString() ?: ""
2737-                        heightStr = imageDimensions?.height?.toString() ?: ""
2738-                        onConfigChange(null)
2739:                    }) { Text(stringResource(R.string.reset)) }
2740-                    Spacer(Modifier.weight(1f))
2741:                    TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
2742-                    Button(onClick = {
2743-                        if (isPercentage) {
2744-                            onConfigChange(com.kompact.model.ResizeConfig(currentPct.toInt(), currentPct.toInt(), maintainAspect, true))
2745-                        } else {
2746-                            val w = widthStr.toIntOrNull() ?: 0
2747-                            val h = heightStr.toIntOrNull() ?: 0
2748-                            if (w > 0 || h > 0) {
2749-                                onConfigChange(com.kompact.model.ResizeConfig(w, h, maintainAspect, false))
2750-                            }
2751-                        }
2752-                        onDismiss()
2753:                    }) { Text(stringResource(R.string.apply)) }
2754-                }
2755-            }
2756-        }
2757-    }
2758-}
2759-
2760-@Composable
2761-fun FiltersEditorDialog(
2762-    currentColorConfig: com.kompact.model.ColorFilterConfig?,
2763-    currentBackgroundConfig: com.kompact.model.BackgroundConfig?,
--
2789-        ) {
2790-            Column(
2791-                modifier = Modifier.padding(16.dp).verticalScroll(androidx.compose.foundation.rememberScrollState())
2792-            ) {
2793-                Row(
2794-                    modifier = Modifier.fillMaxWidth(),
2795-                    horizontalArrangement = Arrangement.SpaceBetween,
2796-                    verticalAlignment = Alignment.CenterVertically
2797-                ) {
2798-                    Text(
2799:                        text = stringResource(R.string.action_color_filter),
2800-                        style = MaterialTheme.typography.titleLarge,
2801-                        color = MaterialTheme.colorScheme.onSurface
2802-                    )
2803-                    androidx.compose.material3.IconButton(onClick = onDismiss) {
2804-                        Icon(androidx.compose.material.icons.Icons.Default.Close, contentDescription = "Close")
2805-                    }
2806-                }
2807-                
2808-                Spacer(Modifier.height(16.dp))
2809-
--
2823-                ) {
2824-                    if (previewUri != null) {
2825-                        coil.compose.AsyncImage(
2826-                            model = previewUri,
2827-                            contentDescription = "Preview",
2828-                            modifier = Modifier.fillMaxSize(),
2829-                            contentScale = ContentScale.Fit,
2830-                            colorFilter = localColorConfig.toComposeColorFilter()
2831-                        )
2832-                    } else {
2833:                        Text(stringResource(R.string.no_preview_available))
2834-                    }
2835-                }
2836-
2837-                Spacer(Modifier.height(16.dp))
2838-                
2839-                androidx.compose.foundation.lazy.LazyRow(
2840-                    modifier = Modifier.fillMaxWidth(),
2841-                    horizontalArrangement = Arrangement.spacedBy(16.dp),
2842-                    contentPadding = PaddingValues(horizontal = 8.dp)
2843-                ) {
--
2900-                                        model = previewUri ?: R.drawable.ic_launcher,
2901-                                        contentDescription = item,
2902-                                        modifier = Modifier.fillMaxSize(),
2903-                                        contentScale = ContentScale.Crop,
2904-                                        colorFilter = com.kompact.model.ColorFilterConfig(filterType = fType).toComposeColorFilter()
2905-                                    )
2906-                                }
2907-                            }
2908-                            Spacer(Modifier.height(8.dp))
2909-                            Text(
2910:                                text = if (item == "NONE") stringResource(R.string.default_text) else if (item == "CUSTOM") "Color" else item.lowercase().replaceFirstChar { it.uppercase() },
2911-                                style = MaterialTheme.typography.labelSmall,
2912-                                maxLines = 1,
2913-                                textAlign = androidx.compose.ui.text.style.TextAlign.Center,
2914-                                color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
2915-                            )
2916-                        }
2917-                    }
2918-                }
2919-                
2920-                Spacer(Modifier.height(16.dp))
2921-
2922-                if (selectedTab == "BACKGROUND") {
2923-                    Text(
2924:                        text = stringResource(R.string.background_type),
2925-                        style = MaterialTheme.typography.labelMedium,
2926-                        color = MaterialTheme.colorScheme.onSurfaceVariant
2927-                    )
2928-                    Spacer(Modifier.height(8.dp))
2929-                    
2930-                    if (showJpegWarning && localBgConfig.type == com.kompact.model.BackgroundType.TRANSPARENT) {
2931-                        Text(
2932:                            text = stringResource(R.string.warning_jpeg_does_not_support_transparency),
2933-                            style = MaterialTheme.typography.bodySmall,
2934-                            color = MaterialTheme.colorScheme.error,
2935-                            modifier = Modifier.padding(bottom = 8.dp)
2936-                        )
2937-                    }
2938-
2939-                    Row(
2940-                        modifier = Modifier.fillMaxWidth().horizontalScroll(androidx.compose.foundation.rememberScrollState()),
2941-                        horizontalArrangement = Arrangement.SpaceEvenly
2942-                    ) {
--
2980-                                    type = com.kompact.model.BackgroundType.CUSTOM,
2981-                                    customColor = android.graphics.Color.RED
2982-                                )
2983-                            }
2984-                        )
2985-                    }
2986-                } 
2987-                
2988-                if (selectedTab == "CUSTOM") {
2989-                    Text(
2990:                        text = stringResource(R.string.hue),
2991-                        style = MaterialTheme.typography.labelMedium,
2992-                        color = MaterialTheme.colorScheme.onSurfaceVariant
2993-                    )
2994-                    
2995-                    val gradientBrush = androidx.compose.ui.graphics.Brush.horizontalGradient(
2996-                        colors = listOf(
2997-                            androidx.compose.ui.graphics.Color.Red,
2998-                            androidx.compose.ui.graphics.Color.Yellow,
2999-                            androidx.compose.ui.graphics.Color.Green,
3000-                            androidx.compose.ui.graphics.Color.Cyan,
--
3033-                    )
3034-                }
3035-
3036-                Spacer(Modifier.height(24.dp))
3037-                Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
3038-                    TextButton(onClick = { 
3039-                        selectedTab = "NONE"
3040-                        localColorConfig = com.kompact.model.ColorFilterConfig() 
3041-                        localBgConfig = com.kompact.model.BackgroundConfig()
3042-                    }) {
3043:                        Text(stringResource(R.string.reset))
3044-                    }
3045-                    Spacer(Modifier.weight(1f))
3046-                    TextButton(onClick = onDismiss) {
3047:                        Text(stringResource(android.R.string.cancel))
3048-                    }
3049-                    TextButton(
3050-                        onClick = {
3051-                            val colorRes = if (localColorConfig.filterType == com.kompact.model.ColorFilterType.NONE) null else localColorConfig
3052-                            val bgRes = if (localBgConfig.type == com.kompact.model.BackgroundType.TRANSPARENT) null else localBgConfig
3053-                            onConfirm(colorRes, bgRes)
3054-                            onDismiss()
3055-                        }
3056-                    ) {
3057:                        Text(stringResource(android.R.string.ok))
3058-                    }
3059-                }
3060-            }
3061-        }
3062-    }
3063-}
3064-
3065-@Composable
3066-fun RotateEditorDialog(
3067-    currentRotation: Float,
--
3084-            shape = RoundedCornerShape(16.dp)
3085-        ) {
3086-            Column(
3087-                modifier = Modifier.padding(16.dp).verticalScroll(androidx.compose.foundation.rememberScrollState())
3088-            ) {
3089-                Row(
3090-                    modifier = Modifier.fillMaxWidth(),
3091-                    horizontalArrangement = Arrangement.SpaceBetween,
3092-                    verticalAlignment = Alignment.CenterVertically
3093-                ) {
3094:                    Text(stringResource(R.string.action_rotate), style = MaterialTheme.typography.titleLarge)
3095-                    androidx.compose.material3.IconButton(onClick = onDismiss) {
3096-                        Icon(androidx.compose.material.icons.Icons.Default.Close, contentDescription = "Close")
3097-                    }
3098-                }
3099-                Spacer(Modifier.height(16.dp))
3100-
3101-                Box(
3102-                    modifier = Modifier
3103-                        .heightIn(max = maxImageHeight)
3104-                        .fillMaxWidth(),
--
3108-                        coil.compose.AsyncImage(
3109-                            model = previewUri,
3110-                            contentDescription = "Preview",
3111-                            modifier = Modifier
3112-                                .fillMaxSize()
3113-                                .rotate(localRotation),
3114-                            contentScale = ContentScale.Fit,
3115-                            colorFilter = colorFilter?.toComposeColorFilter()
3116-                        )
3117-                    } else {
3118:                        Text(stringResource(R.string.no_preview_available))
3119-                    }
3120-                }
3121-
3122-                Spacer(Modifier.height(16.dp))
3123-                
3124-                Text(text = "Degrees: ${localRotation.toInt()}°", style = MaterialTheme.typography.bodyMedium)
3125-                androidx.compose.material3.Slider(
3126-                    value = localRotation,
3127-                    onValueChange = { localRotation = it },
3128-                    valueRange = -180f..180f,
--
3138-                        val rem = kotlin.math.abs((localRotation % 90f).toInt())
3139-                        if (rem != 0) {
3140-                            localRotation = -90f
3141-                        } else {
3142-                            localRotation -= 90f
3143-                            if (localRotation < -180f) localRotation += 360f
3144-                        }
3145-                    }) { 
3146-                        Icon(androidx.compose.material.icons.Icons.AutoMirrored.Filled.RotateLeft, contentDescription = null)
3147-                        Spacer(Modifier.width(8.dp))
3148:                        Text(stringResource(R.string.rotate_left)) 
3149-                    }
3150-                    Spacer(Modifier.width(16.dp))
3151-                    Button(onClick = { 
3152-                        val rem = kotlin.math.abs((localRotation % 90f).toInt())
3153-                        if (rem != 0) {
3154-                            localRotation = 90f
3155-                        } else {
3156-                            localRotation += 90f
3157-                            if (localRotation > 180f) localRotation -= 360f
3158-                        }
3159-                    }) { 
3160-                        Icon(androidx.compose.material.icons.Icons.AutoMirrored.Filled.RotateRight, contentDescription = null)
3161-                        Spacer(Modifier.width(8.dp))
3162:                        Text(stringResource(R.string.rotate_right)) 
3163-                    }
3164-                }
3165-
3166-                Spacer(Modifier.height(16.dp))
3167-                Row(
3168-                    modifier = Modifier.fillMaxWidth()
3169-                ) {
3170:                    TextButton(onClick = { localRotation = 0f }) { Text(stringResource(R.string.reset)) }
3171-                    Spacer(Modifier.weight(1f))
3172:                    TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
3173-                    Button(onClick = {
3174-                        onConfirm(localRotation)
3175-                        onDismiss()
3176:                    }) { Text(stringResource(R.string.apply)) }
3177-                }
3178-            }
3179-        }
3180-    }
3181-}
3182-
3183-@Composable
3184-fun BackgroundOption(
3185-    label: String,
3186-    color: androidx.compose.ui.graphics.Color,
--
3197-                .clip(CircleShape)
3198-                .background(if (color == androidx.compose.ui.graphics.Color.Transparent) MaterialTheme.colorScheme.surfaceVariant else color)
3199-                .border(
3200-                    width = if (isSelected) 3.dp else 1.dp,
3201-                    color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
3202-                    shape = CircleShape
3203-                ),
3204-            contentAlignment = Alignment.Center
3205-        ) {
3206-            if (color == androidx.compose.ui.graphics.Color.Transparent) {
3207:                Text(stringResource(R.string.text_tool), style = MaterialTheme.typography.bodySmall)
3208-            }
3209-        }
3210-        Spacer(modifier = Modifier.height(4.dp))
3211-        Text(text = label, style = MaterialTheme.typography.labelSmall)
3212-    }
3213-}
